Pisanie czytelnego kodu dla VBA – wzór Try*
Ostatnio znalazłem się za pomocą Try
wzór coraz więcej. Bardzo podoba mi się ten wzorzec, ponieważ sprawia, że kod jest znacznie bardziej czytelny. Jest to szczególnie ważne podczas programowania w dojrzałym języku programowania, takim jak VBA, w którym obsługa błędów jest spleciona z przepływem sterowania. Ogólnie uważam, że trudniej jest przestrzegać procedur, które opierają się na obsłudze błędów jako przepływie sterowania.
Scenariusz
Zacznijmy od przykładu. Model obiektowy DAO jest idealnym kandydatem ze względu na sposób działania. Zobacz, wszystkie obiekty DAO mają Properties
kolekcja, która zawiera Property
przedmioty. Jednak każdy może dodać właściwość niestandardową. W rzeczywistości program Access doda kilka właściwości do różnych obiektów DAO. Dlatego możemy mieć właściwość, która może nie istnieć i musimy obsługiwać zarówno przypadek zmiany wartości istniejącej właściwości, jak i przypadek dodania nowej właściwości.
Użyjmy Subdatasheet
nieruchomość jako przykład. Domyślnie wszystkie tabele utworzone za pomocą interfejsu użytkownika programu Access będą miały właściwość ustawioną na Auto
, ale możemy tego nie chcieć. Ale jeśli mamy tabele utworzone w kodzie lub w inny sposób, może nie mieć właściwości. Możemy więc zacząć od początkowej wersji kodu, aby zaktualizować właściwości wszystkich tabel i zająć się obydwoma przypadkami.
Public Sub EditTableSubdatasheetProperty( _ Opcjonalne NewValue As String ="[Brak]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Błąd On GoTo Ustaw db =CurrentDb dla każdego tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Nie tdf.Name Like "~*") Then 'Nie dołączony lub temp . Set prp =tdf.Properties(SubDatasheetPropertyName) If prp.Value <> NewValue Then prp.Value =NewValue End If End If End IfContinue:NextExitProc:Exit SubErrHandler:If Err.Number =3270 Then Set prp =tdf.CreateProperty( dbText, NewValue) tdf.Properties.Append prp Wznów Kontynuuj Koniec Jeśli MsgBox Err.Number &":" &Err.Description Wznów ExitProc End Sub
Kod prawdopodobnie zadziała. Jednak, aby to zrozumieć, prawdopodobnie musimy naszkicować jakiś schemat blokowy. Wiersz Set prp = tdf.Properties(SubDatasheetPropertyName)
może potencjalnie zgłosić błąd 3270. W takim przypadku formant przeskakuje do sekcji obsługi błędów. Następnie tworzymy właściwość i wznawiamy działanie w innym punkcie pętli, używając etykiety Continue
. Jest kilka pytań…
- Co jeśli 3270 zostanie podniesione w innej linii?
- Załóżmy, że wiersz
Set prp =...
nie rzuca błąd 3270, ale w rzeczywistości jakiś inny błąd? - Co się stanie, jeśli gdy jesteśmy w module obsługi błędów, wystąpi kolejny błąd podczas wykonywania
Append
lubCreateProperty
? - Czy ta funkcja powinna nawet wyświetlać
Msgbox
? ? Pomyśl o funkcjach, które mają działać na coś w imieniu formularzy lub przycisków. Jeśli funkcje wyświetlają okno komunikatu, a następnie wychodzą normalnie, kod wywołujący nie ma pojęcia, że coś poszło nie tak i może kontynuować robienie rzeczy, których nie powinien robić. - Czy możesz rzucić okiem na kod i natychmiast zrozumieć, co on robi? nie mogę. Muszę na to zmrużyć oczy, potem pomyśleć, co powinno się stać w przypadku błędu i naszkicować w myślach ścieżkę. To nie jest łatwe do odczytania.
Dodaj HasProperty
procedura
Czy możemy zrobić lepiej? Tak! Niektórzy programiści już rozpoznają problem z obsługą błędów, jak to zilustrowałem, i mądrze wyodrębnili to do własnej funkcji. Oto lepsza wersja:
Public Sub EditTableSubdatasheetProperty( _ Opcjonalne NewValue As String ="[Brak]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameb" Set db =CurrentD Dla każdego tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Nie tdf.Name Like "~*") Then 'Nie dołączony lub temp. If Not HasProperty(tdf, SubDatasheetPropertyName) Następnie ustaw prp =tdf.CreateProperty(SubDatasheetPropertyName , dbNameText, NewValue) tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyName) <> NewFalueNoweValue Then tdf.Properties. End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Ignorowane As Variant On Error Resume Next Ignored =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End Function
Zamiast mieszać przepływ wykonania z obsługą błędów, mamy teraz funkcję HasFunction
który zgrabnie wyodrębnia podatne na błędy sprawdzenie właściwości, która może nie istnieć. W konsekwencji nie potrzebujemy skomplikowanego przepływu obsługi/wykonywania błędów, który widzieliśmy w pierwszym przykładzie. Jest to duża poprawa i sprawia, że kod jest nieco czytelny. Ale…
- Mamy jedną gałąź, która używa zmiennej
prp
i mamy inną gałąź, która używatdf.Properties(SubDatasheetPropertyName)
to w rzeczywistości odnosi się do tej samej właściwości. Dlaczego powtarzamy się z dwoma różnymi sposobami odwoływania się do tej samej właściwości? - Często zajmujemy się nieruchomością.
HasProperty
musi obsłużyć właściwość, aby dowiedzieć się, czy istnieje, po prostu zwracaBoolean
wynik, pozostawiając kodowi wywołującemu ponowną próbę uzyskania tej samej właściwości w celu zmiany wartości. - Podobnie zajmujemy się
NewValue
więcej niż to konieczne. Przekażemy go wCreateProperty
lub ustawValue
własność nieruchomości. HasProperty
funkcja niejawnie zakłada, że obiekt maProperties
członka i nazywa go późnym wiązaniem, co oznacza, że jest to błąd w czasie wykonywania, jeśli dostarczono mu niewłaściwy rodzaj obiektu.
Użyj TryGetProperty
zamiast tego
Czy możemy zrobić lepiej? Tak! W tym miejscu musimy przyjrzeć się wzorowi Try. Jeśli kiedykolwiek programowałeś w .NET, prawdopodobnie widziałeś metody takie jak TryParse
gdzie zamiast zgłaszać błąd w przypadku niepowodzenia, możemy ustawić warunek, aby zrobić coś dla sukcesu i coś innego dla porażki. Ale co ważniejsze, mamy wynik do osiągnięcia sukcesu. Jak więc ulepszylibyśmy HasProperty
funkcjonować? Po pierwsze, powinniśmy zwrócić Property
obiekt. Wypróbujmy ten kod:
Funkcja publiczna TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName As String, _ ByRef OutProperty As DAO.Property _) As Boolean On Error Resume Next Ustaw OutProperty =SourceProperties(PropertyName) Jeśli Err.Number Następnie ustaw OutProperty =Nic nie kończy się w przypadku błędu GoTo 0 TryGetProperty =(Nie OutProperty to nic) Koniec funkcji
Z kilkoma zmianami odnieśliśmy kilka dużych zwycięstw:
- Dostęp do
Properties
nie jest już spóźniony. Nie musimy mieć nadziei, że obiekt ma właściwość o nazwieProperties
i jest toDAO.Properties
. Można to zweryfikować w czasie kompilacji. - Zamiast tylko
Boolean
wynik, możemy również uzyskać pobranąProperty
obiekt, ale tylko na sukces. Jeśli nam się nie uda,OutProperty
parametrem będzieNothing
. Nadal będziemy używaćBoolean
wynik, który pomoże w ustawieniu przepływu, jak wkrótce zobaczysz. - Nadając naszej nowej funkcji nazwę
Try
prefiksu, wskazujemy, że gwarantuje to brak błędu w normalnych warunkach pracy. Oczywiście nie możemy zapobiec błędom braku pamięci lub coś w tym rodzaju, ale w tym momencie mamy znacznie większe problemy. Ale w normalnych warunkach operacyjnych uniknęliśmy plątania naszej obsługi błędów z przepływem wykonywania. Kod można teraz odczytać od góry do dołu bez przeskakiwania w przód lub w tył.
Zauważ, że zgodnie z konwencją poprzedzam właściwość „out” przedrostkiem Out
. To pomaga wyjaśnić, że mamy przekazać zmienną do niezainicjowanej funkcji. Oczekujemy również, że funkcja zainicjuje parametr. Będzie to jasne, gdy spojrzymy na kod wywołujący. Skonfigurujmy więc kod dzwonienia.
Zmieniony kod wywołujący przy użyciu TryGetProperty
Public Sub EditTableSubdatasheetProperty( _ Opcjonalne NewValue As String ="[Brak]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameb" Set db =CurrentD Dla każdego tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Nie tdf.Name Like "~*") Then 'Nie dołączony lub temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then If prp.Value <> NewValue Then prp.Value =NewValue End If Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tddfend.Properties If. Jeśli następny koniec Sub
Kod jest teraz nieco bardziej czytelny z pierwszym wzorcem Try. Udało nam się zmniejszyć obsługę prp
. Zauważ, że przekazujemy prp
do zmiennej prp
zostanie zainicjowany z właściwością, którą chcemy manipulować. W przeciwnym razie prp
pozostaje Nothing
. Następnie możemy użyć CreateProperty
aby zainicjować prp
zmienna.
Odwróciliśmy również negację, aby kod stał się łatwiejszy do odczytania. Jednak tak naprawdę nie zmniejszyliśmy obsługi NewValue
parametr. Nadal mamy kolejny zagnieżdżony blok do sprawdzenia wartości. Czy możemy zrobić lepiej? Tak! Dodajmy kolejną funkcję:
Dodawanie TrySetPropertyValue
procedura
Funkcja publiczna TrySetPropertyValue( _ ByVal SourceProperty As DAO.Property, _ ByVal NewValue As Variant_) As Boolean If SourceProperty.Value =PropertyValue Then TrySetPropertyValue =True Else W przypadku błędu Resume Next =SourceProperty.Value On Error 0 SourceProperty.Value =NewValue) Zakończ funkcję IfEnd
Ponieważ gwarantujemy, że ta funkcja nie wygeneruje błędu podczas zmiany wartości, nazywamy ją TrySetPropertyValue
. Co ważniejsze, ta funkcja pomaga zawrzeć wszystkie krwawe szczegóły dotyczące zmiany wartości nieruchomości. Mamy sposób, aby zagwarantować, że wartość jest taka, jakiej się spodziewaliśmy. Przyjrzyjmy się, jak kod wywołujący zostanie zmieniony za pomocą tej funkcji.
Zaktualizowano kod wywołujący przy użyciu obu opcji TryGetProperty
i TrySetPropertyValue
Public Sub EditTableSubdatasheetProperty( _ Opcjonalne NewValue As String ="[Brak]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameb" Set db =CurrentD Dla każdego tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Nie tdf.Name Like "~*") Then 'Nie dołączony lub temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Ustaw prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Sub prp End If End If End> NextWyeliminowaliśmy cały
If
blok. Teraz możemy po prostu przeczytać kod i od razu, że próbujemy ustawić wartość właściwości, a jeśli coś pójdzie nie tak, po prostu idziemy dalej. Jest to o wiele łatwiejsze do odczytania, a nazwa funkcji sama się opisuje. Dobra nazwa sprawia, że wyszukiwanie definicji funkcji jest mniej konieczne, aby zrozumieć, co ona robi.Tworzenie
TryCreateOrSetProperty
proceduraKod jest bardziej czytelny, ale nadal mamy to
Else
blok tworzenia właściwości. Czy możemy zrobić jeszcze lepiej? Tak! Zastanówmy się, co musimy tutaj osiągnąć. Mamy własność, która może, ale nie musi istnieć. Jeśli tak się nie stanie, chcemy to stworzyć. Niezależnie od tego, czy już istniał, czy nie, potrzebujemy go ustawić na określoną wartość. Potrzebujemy więc funkcji, która albo utworzy właściwość, albo zaktualizuje wartość, jeśli już istnieje. Aby utworzyć właściwość, musimy wywołaćCreateProperty
którego niestety nie ma wProperties
ale raczej różne obiekty DAO. Dlatego musimy późne wiązanie za pomocąObject
typ danych. Jednak nadal możemy zapewnić pewne kontrole w czasie wykonywania, aby uniknąć błędów. StwórzmyTryCreateOrSetProperty
funkcja:Funkcja publiczna TryCreateOrSetProperty( _ ByVal SourceDaoObject As Object, _ ByVal PropertyName As String, _ ByVal PropertyType As DAO.DataTypeEnum, _ ByVal PropertyValue As Variant, _ ByRef OutProperty As DAO.Property _) TrueD Case Type Boolean Select Case To DAO.TableDef, _ TypeOf SourceDaoObject To DAO.QueryDef, _ TypeOf SourceDaoObject To DAO.Field, _ TypeOf SourceDaoObject To DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertySetName, OutProtyVertyOrtyWłaściwość) Then Błąd Wznów Dalej Ustaw OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty Jeśli Err.Number Następnie ustaw OutProperty =Nic nie kończ, jeśli włączone Błąd GoTo 0 TryCreateOrSetProperty =(OutProperty to nic) End If Case Else Err.Raise 5, , "Nieprawidłowy obiekt dostarczony do parametru SourceDaoObject. Musi to być obiekt DAO zawierający element CreateProperty." Zakończ funkcję SelectEndKilka rzeczy do zapamiętania:
- Udało nam się zbudować na wcześniejszej
Try*
zdefiniowaną przez nas funkcję, która pomaga ograniczyć kodowanie treści funkcji, pozwalając jej skupić się bardziej na tworzeniu w przypadku braku takiej właściwości. - Jest to z konieczności bardziej szczegółowe ze względu na dodatkowe kontrole w czasie wykonywania, ale jesteśmy w stanie ustawić to tak, aby błędy nie zmieniały przepływu wykonywania i nadal możemy czytać od góry do dołu bez przeskakiwania.
- Zamiast rzucać
MsgBox
znikąd używamyErr.Raise
i zwróć znaczący błąd. Właściwa obsługa błędów jest delegowana do kodu wywołującego, który może następnie zdecydować, czy pokazać użytkownikowi okno komunikatu, czy zrobić coś innego. - Ze względu na naszą ostrożną obsługę i zapewnienie, że
SourceDaoObject
parametr jest prawidłowy, każda możliwa ścieżka gwarantuje, że wszelkie problemy z utworzeniem lub ustawieniem wartości istniejącej właściwości zostaną rozwiązane i otrzymamyfalse
wynik. Ma to wpływ na kod wywołujący, jak wkrótce zobaczymy.
Ostateczna wersja kodu wywołującego
Zaktualizujmy kod wywołujący, aby korzystać z nowej funkcji:
Public Sub EditTableSubdatasheetProperty( _ Opcjonalne NewValue As String ="[Brak]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameb" Set db =CurrentD Dla każdego tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Nie tdf.Name Like "~*") Then 'Nie dołączony lub temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub
To była spora poprawa czytelności. W oryginalnej wersji musielibyśmy przeanalizować kilka If
bloki i jak obsługa błędów zmienia przepływ wykonywania. Musielibyśmy dowiedzieć się, co dokładnie robi treść, aby dojść do wniosku, że próbujemy uzyskać właściwość lub ją utworzyć, jeśli ona nie istnieje i ma ustawioną określoną wartość. W obecnej wersji wszystko znajduje się w nazwie funkcji, TryCreateOrSetProperty
. Teraz możemy zobaczyć, czego oczekuje się od funkcji.
Wniosek
Możesz się zastanawiać, „ale dodaliśmy znacznie więcej funkcji i znacznie więcej linii. Czy to nie jest dużo pracy?” To prawda, że w obecnej wersji zdefiniowaliśmy jeszcze 3 funkcje. Możesz jednak przeczytać każdą pojedynczą funkcję osobno i nadal łatwo zrozumieć, co powinna zrobić. Widziałeś również, że TryCreateOrSetProperty
funkcja może zostać rozbudowana na 2 innych Try*
Funkcje. Oznacza to, że mamy większą elastyczność w łączeniu logiki.
Więc jeśli napiszemy inną funkcję, która robi coś z właściwością obiektów, nie musimy jej pisać od nowa ani nie kopiujemy i wklejamy kodu z oryginalnego EditTableSubdatasheetProperty
do nowej funkcji. W końcu nowa funkcja może wymagać różnych wariantów, a tym samym wymagać innej kolejności. Na koniec pamiętaj, że prawdziwymi beneficjentami są kod wywołujący, który musi coś zrobić. Chcemy utrzymać kod wywołujący na dość wysokim poziomie, bez gubienia się w szczegółach, które mogą być szkodliwe dla konserwacji.
Widać również, że obsługa błędów jest znacznie uproszczona, mimo że użyliśmy opcji On Error Resume Next
. Nie musimy już szukać kodu błędu, ponieważ w większości przypadków interesuje nas tylko to, czy się udało, czy nie. Co ważniejsze, obsługa błędów nie zmieniła przepływu wykonywania, w którym masz pewną logikę w treści i inną logikę w obsłudze błędów. To ostatnie jest sytuacją, której zdecydowanie chcemy uniknąć, ponieważ jeśli w module obsługi błędów wystąpi błąd, zachowanie może być zaskakujące. Najlepiej tego uniknąć.
Chodzi o abstrakcję
Ale najważniejszy wynik, jaki tu zdobywamy, to poziom abstrakcji, jaki możemy teraz osiągnąć. Oryginalna wersja EditTableSubdatasheetProperty
zawierał wiele niskopoziomowych szczegółów dotyczących obiektu DAO, tak naprawdę nie dotyczy podstawowego celu funkcji. Pomyśl o dniach, w których widziałeś procedurę o długości setek linii z głęboko zagnieżdżonymi pętlami lub warunkami. Czy chciałbyś to debugować? nie.
Więc kiedy widzę procedurę, pierwszą rzeczą, którą naprawdę chcę zrobić, to wyrzucić części do ich własnej funkcji, aby móc podnieść poziom abstrakcji dla tej procedury. Zmuszając się do podnoszenia poziomu abstrakcji, możemy również uniknąć dużych klas błędów, których przyczyną jest to, że jedna zmiana w części megaprocedury ma niezamierzone konsekwencje dla innych części procedur. Kiedy wywołujemy funkcje i przekazujemy parametry, zmniejszamy również możliwość niepożądanych efektów ubocznych zakłócających naszą logikę.
Dlatego uwielbiam wzór „Wypróbuj*”. Mam nadzieję, że uznasz to również za przydatne w Twoich projektach.