Access
 sql >> Baza danych >  >> RDS >> Access

Pisanie czytelnego kodu dla VBA – wzór Try*

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 lub CreateProperty ?
  • 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żywa tdf.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 zwraca Boolean 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 w CreateProperty lub ustaw Value własność nieruchomości.
  • HasProperty funkcja niejawnie zakłada, że ​​obiekt ma Properties 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 nazwie Properties i jest to DAO.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ędzie Nothing . 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 true , 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> Next 

Wyeliminowaliś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 procedura

Kod 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 w Properties 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órzmy TryCreateOrSetProperty 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ę SelectEnd

Kilka 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żywamy Err.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 otrzymamy false 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. 10 wskazówek oszczędzających czas dla użytkowników MS Access

  2. Jak uzyskać najnowsze funkcje w Office 365

  3. 5 powodów, dla których Microsoft Access jest świetny dla start-upów

  4. Jakie są 10 najlepszych funkcji programu Microsoft Access?

  5. Dołącz do nas, aby uzyskać wstęp do programu SQL Server