Leesbare code schrijven voor VBA – Try* patroon
De laatste tijd merk ik dat ik de Try
. gebruik patroon steeds meer. Ik hou echt van dit patroon omdat het zorgt voor veel leesbare code. Dit is vooral belangrijk bij het programmeren in een volwassen programmeertaal zoals VBA, waar de foutafhandeling verweven is met de besturingsstroom. Over het algemeen vind ik procedures die afhankelijk zijn van foutafhandeling als controlestroom moeilijker te volgen.
Scenario
Laten we beginnen met een voorbeeld. DAO-objectmodel is een perfecte kandidaat vanwege de manier waarop het werkt. Kijk, alle DAO-objecten hebben Properties
collectie, die Property
. bevat voorwerpen. Iedereen kan echter aangepaste eigenschappen toevoegen. In feite voegt Access verschillende eigenschappen toe aan verschillende DAO-objecten. Daarom is het mogelijk dat we een eigenschap hebben die mogelijk niet bestaat en die zowel het geval van het wijzigen van de waarde van een bestaande eigenschap als het toevoegen van een nieuwe eigenschap moeten behandelen.
Laten we Subdatasheet
gebruiken eigendom als voorbeeld. Standaard hebben alle tabellen die zijn gemaakt via de gebruikersinterface van Access de eigenschap ingesteld op Auto
, maar dat willen we misschien niet. Maar als we tabellen hebben die in code of op een andere manier zijn gemaakt, heeft deze mogelijk niet de eigenschap. We kunnen dus beginnen met een eerste versie van de code om de eigenschappen van alle tabellen bij te werken en beide gevallen af te handelen.
Public Sub EditTableSubdatasheetProperty( _ Optioneel NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" On Error Stel db =CurrentDb in voor elke tdf in db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Not Attached, or 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.SubData dbText, NewValue) tdf.Properties.Append prp Hervatten Doorgaan End If MsgBox Err.Number &":" &Err.Description Hervatten ExitProc End Sub
De code zal waarschijnlijk werken. Om het echter te begrijpen, moeten we waarschijnlijk een stroomschema schetsen. De regel Set prp = tdf.Properties(SubDatasheetPropertyName)
kan mogelijk een fout 3270 veroorzaken. In dit geval springt de besturing naar de sectie voor foutafhandeling. We maken dan een eigenschap en gaan dan verder op een ander punt van de lus met het label Continue
. Er zijn enkele vragen...
- Wat als 3270 op een andere regel wordt geraised?
- Stel dat de regel
Set prp =...
gooit niet fout 3270 maar eigenlijk een andere fout? - Wat als, terwijl we ons in de fout-handler bevinden, een andere fout optreedt bij het uitvoeren van de
Append
ofCreateProperty
? - Moet deze functie zelfs een
Msgbox
tonen? ? Denk aan functies die aan iets zouden moeten werken namens formulieren of knoppen. Als de functies een berichtvenster tonen, sluit dan normaal af. De aanroepende code heeft geen idee dat er iets mis is gegaan en kan dingen blijven doen die het niet zou moeten doen. - Kun je een blik werpen op de code en meteen begrijpen wat deze doet? ik kan het niet. Ik moet ernaar kijken, dan nadenken over wat er moet gebeuren in het geval van een fout en mentaal het pad schetsen. Dat is niet gemakkelijk te lezen.
Voeg een HasProperty
toe procedure
Kunnen we het beter doen? Ja! Sommige programmeurs herkennen het probleem met het gebruik van foutafhandeling al, zoals ik heb geïllustreerd, en hebben dit wijselijk geabstraheerd in zijn eigen functie. Hier is een betere versie:
Public Sub EditTableSubdatasheetProperty( _ Optioneel NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameDb" Set db ="SubdatasheetNameDb" Voor elke tdf in db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Dan 'Niet bijgevoegd, of temp. If Not HasProperty(tdf, SubDatasheetPropertyName) Stel vervolgens prp =tdf.CreateProperty(SubDatasheetPropertyName, dbText, NewValue) in tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyName) <> NewPropertyName End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Genegeerd als variant bij fout Hervatten Next Ignored =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End Function
In plaats van de uitvoeringsstroom te verwarren met de foutafhandeling, hebben we nu een functie HasFunction
die de foutgevoelige controle voor een eigenschap die mogelijk niet bestaat, netjes abstraheert. Als gevolg hiervan hebben we geen complexe foutafhandeling / uitvoeringsstroom nodig die we in het eerste voorbeeld zagen. Dit is een grote verbetering en zorgt voor enigszins leesbare code. Maar…
- We hebben één branch die de variabele
prp
. gebruikt en we hebben nog een branch dietdf.Properties(SubDatasheetPropertyName)
gebruikt dat in feite verwijst naar dezelfde eigenschap. Waarom herhalen we onszelf met twee verschillende manieren om naar dezelfde eigenschap te verwijzen? - We zijn nogal veel bezig met het onroerend goed. De
HasProperty
moet de eigenschap afhandelen om erachter te komen of deze bestaat en retourneert eenvoudig eenBoolean
resultaat, laat het aan de aanroepende code over om opnieuw te proberen dezelfde eigenschap opnieuw te krijgen om de waarde te wijzigen. - Op dezelfde manier behandelen we de
NewValue
meer dan nodig. We geven het door in deCreateProperty
of stel deValue
in eigendom van het pand. - De
HasProperty
functie veronderstelt impliciet dat het object eenProperties
. heeft member en noemt het laat-gebonden, wat betekent dat het een runtime-fout is als er een verkeerd soort object aan wordt geleverd.
Gebruik TryGetProperty
in plaats daarvan
Kunnen we het beter doen? Ja! Dat is waar we naar het Try-patroon moeten kijken. Als je ooit met .NET hebt geprogrammeerd, heb je waarschijnlijk methoden gezien zoals TryParse
waar in plaats van een fout te melden bij een mislukking, we een voorwaarde kunnen stellen om iets te doen voor succes en iets anders voor mislukking. Maar nog belangrijker, we hebben het resultaat beschikbaar voor succes. Dus hoe zouden we de HasProperty
. verbeteren? functie? Om te beginnen moeten we de Property
. retourneren voorwerp. Laten we deze code proberen:
Public Function TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName As String, _ ByRef OutProperty As DAO.Property _) As Boolean On Error Hervatten Volgende Set OutProperty =SourceProperties(PropertyName) Als Err.Number Stel OutProperty in =Niets eindigt bij fout GoTo 0 TryGetProperty =(Niet OutProperty is niets) Functie beëindigen
Met een paar veranderingen hebben we weinig grote overwinningen behaald:
- De toegang tot
Properties
is niet meer laatgebonden. We hoeven niet te hopen dat een object een eigenschap heeft met de naamProperties
en het is vanDAO.Properties
. Dit kan worden geverifieerd tijdens het compileren. - In plaats van alleen een
Boolean
resultaat, kunnen we ook de opgehaaldeProperty
. krijgen object, maar alleen op het succes. Als we falen, deOutProperty
parameter isNothing
. We gebruiken nog steeds deBoolean
resultaat om te helpen bij het instellen van de opwaartse stroom, zoals u binnenkort zult zien. - Door onze nieuwe functie een naam te geven met
Try
prefix, geven we aan dat dit gegarandeerd geen fout veroorzaakt onder normale bedrijfsomstandigheden. Het is duidelijk dat we geheugenfouten of iets dergelijks niet kunnen voorkomen, maar op dat moment hebben we veel grotere problemen. Maar onder de normale bedrijfsomstandigheden hebben we voorkomen dat onze foutafhandeling met de uitvoeringsstroom in de war raakt. De code kan nu van boven naar beneden worden gelezen zonder vooruit of achteruit te springen.
Merk op dat ik volgens afspraak de eigenschap "out" voorvoeg met Out
. Dat helpt om duidelijk te maken dat we verondersteld worden de variabele door te geven aan de niet-geïnitialiseerde functie. We verwachten ook dat de functie de parameter zal initialiseren. Dat wordt duidelijk als we kijken naar de belcode. Laten we dus de belcode instellen.
Herziene oproepcode met behulp van TryGetProperty
Public Sub EditTableSubdatasheetProperty( _ Optioneel NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameDb" Set db ="SubdatasheetNameDb" Voor elke tdf in db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Dan 'Niet bijgevoegd, of temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then If prp.Value <> NewValue Then prp.Value =NewValue End If Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText Endpr. Als NextEnd Sub
De code is nu wat leesbaarder met het eerste Try-patroon. We zijn erin geslaagd de verwerking van de prp
. te verminderen . Merk op dat we de prp
. doorgeven variabele in de prp
wordt geïnitialiseerd met de eigenschap die we willen manipuleren. Anders, de prp
blijft Nothing
. We kunnen dan de CreateProperty
. gebruiken om de prp
. te initialiseren variabel.
We hebben ook de ontkenning omgedraaid, zodat de code gemakkelijker leesbaar wordt. We hebben de verwerking van NewValue
echter niet echt verminderd parameter. We hebben nog een ander genest blok om de waarde te controleren. Kunnen we het beter doen? Ja! Laten we nog een functie toevoegen:
Toevoegen van TrySetPropertyValue
procedure
Public Function TrySetPropertyValue( _ ByVal SourceProperty As DAO.Property, _ ByVal NewValue As Variant_) As Boolean If SourceProperty.Value =PropertyValue Dan TrySetPropertyValue =True Anders bij fout Hervatten volgende SourceProperty.Value =NewValue SourceProperty.Value =NewValue) End IfEnd-functie
Omdat we garanderen dat deze functie geen foutmelding geeft bij het wijzigen van de waarde, noemen we het TrySetPropertyValue
. Wat nog belangrijker is, deze functie helpt bij het inkapselen van alle bloederige details rond het veranderen van de waarde van het onroerend goed. We hebben een manier om te garanderen dat de waarde de waarde is die we verwachtten. Laten we eens kijken hoe de belcode met deze functie wordt gewijzigd.
Belcode bijgewerkt met beide TryGetProperty
en TrySetPropertyValue
Public Sub EditTableSubdatasheetProperty( _ Optioneel NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameDb" Set db ="SubdatasheetNameDb" Voor elke tdf in db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Dan 'Niet bijgevoegd, of temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties If EndWe hebben een hele
If
. geëlimineerd blok. We kunnen nu gewoon de code lezen en meteen zien dat we een eigenschapswaarde proberen in te stellen en als er iets misgaat, gaan we gewoon door. Dat is veel makkelijker te lezen en de naam van de functie is zelfbeschrijvend. Een goede naam maakt het minder nodig om de definitie van de functie op te zoeken om te begrijpen wat deze doet.
TryCreateOrSetProperty
maken procedureDe code is beter leesbaar, maar we hebben nog steeds die
Else
blok het maken van een eigenschap. Kunnen we het nog beter doen? Ja! Laten we nadenken over wat we hier moeten bereiken. We hebben een pand dat al dan niet bestaat. Als dat niet het geval is, willen we het creëren. Of het nu al bestond of niet, we moeten het op een bepaalde waarde instellen. Dus wat we nodig hebben is een functie die een eigenschap maakt of de waarde bijwerkt als deze al bestaat. Om een eigenschap aan te maken, moeten weCreateProperty
. aanroepen die helaas niet op deProperties
. staat maar eerder verschillende DAO-objecten. We moeten dus laat binden met behulp vanObject
data type. We kunnen echter nog steeds enkele runtime-controles uitvoeren om fouten te voorkomen. Laten we eenTryCreateOrSetProperty
. maken functie:Public Function TryCreateOrSetProperty( _ ByVal SourceDaoObject As Object, _ ByVal PropertyName As String, _ ByVal PropertyType As DAO.DataTypeEnum, _ ByVal PropertyValue As Variant, _ ByRef OutProperty As DAO.Property _) Case TypeOf Source Selecteer Case TrueOb Is DAO.TableDef, _ TypeOf SourceDaoObject Is DAO.QueryDef, _ TypeOf SourceDaoObject Is DAO.Field, _ TypeOf SourceDaoObject Is DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertySetePropertyName, OutProperty) DanPropertyCreateOr Fout Hervatten Volgende Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Set OutProperty =Nothing End If On Fout GoTo 0 TryCreateOrSetProperty =(OutProperty is niets) End If Case Else Err.Raise 5, , "Ongeldig object verstrekt aan de parameter SourceDaoObject. Het moet een DAO-object zijn dat een CreateProperty-lid bevat." End SelectEnd FunctionEnkele dingen om op te merken:
- We konden voortbouwen op de eerdere
Try*
functie die we hebben gedefinieerd, wat helpt om de codering van de body van de functie te verminderen, waardoor deze zich meer kan concentreren op de creatie in het geval dat een dergelijke eigenschap niet bestaat. - Dit is noodzakelijkerwijs uitgebreider vanwege de extra runtime-controles, maar we kunnen het zo instellen dat fouten de uitvoeringsstroom niet veranderen en we nog steeds van boven naar beneden kunnen lezen zonder te springen.
- In plaats van een
MsgBox
te gooien uit het niets gebruiken weErr.Raise
en een betekenisvolle fout retourneren. De daadwerkelijke foutafhandeling wordt gedelegeerd aan de aanroepende code, die vervolgens kan beslissen of een berichtenbox aan de gebruiker moet worden getoond of iets anders moet worden gedaan. - Vanwege onze zorgvuldige behandeling en op voorwaarde dat de
SourceDaoObject
parameter geldig is, garandeert al het mogelijke pad dat eventuele problemen met het maken of instellen van de waarde van een bestaande eigenschap worden afgehandeld en krijgen we eenfalse
resultaat. Dat heeft invloed op de belcode, zoals we binnenkort zullen zien.
Definitieve versie van de oproepcode
Laten we de oproepcode bijwerken om de nieuwe functie te gebruiken:
Public Sub EditTableSubdatasheetProperty( _ Optioneel NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetNameDb" Set db ="SubdatasheetNameDb" Voor elke tdf in db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Dan 'Niet bijgevoegd, of temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub
Dat was een hele verbetering van de leesbaarheid. In de originele versie zouden we een aantal If
. onder de loep moeten nemen blokken en hoe foutafhandeling de uitvoeringsstroom verandert. We zouden moeten uitzoeken wat de inhoud precies aan het doen was om te concluderen dat we proberen een eigenschap te krijgen of deze te creëren als deze niet bestaat en deze op een bepaalde waarde in te stellen. Met de huidige versie staat het allemaal in de naam van de functie, TryCreateOrSetProperty
. We kunnen nu zien wat de functie naar verwachting zal doen.
Conclusie
Je vraagt je misschien af, “maar we hebben veel meer functies en veel meer lijnen toegevoegd. Is dat niet veel werk?” Het is waar dat we in deze huidige versie nog 3 functies hebben gedefinieerd. U kunt echter elke afzonderlijke functie afzonderlijk lezen en toch gemakkelijk begrijpen wat deze moet doen. Je zag ook dat de TryCreateOrSetProperty
functie zou kunnen opbouwen op de 2 andere Try*
functies. Dat betekent dat we meer flexibiliteit hebben bij het samenstellen van de logica.
Dus als we een andere functie schrijven die iets doet met de eigenschap van objecten, hoeven we deze niet helemaal opnieuw te schrijven en evenmin kopiëren en plakken we de code van de originele EditTableSubdatasheetProperty
in de nieuwe functie. De nieuwe functie kan immers verschillende varianten nodig hebben en dus een andere volgorde. Houd er ten slotte rekening mee dat de echte begunstigden de belcode zijn die iets moet doen. We willen de belcode redelijk hoog houden zonder ons te vermoeien met details die slecht kunnen zijn voor het onderhoud.
U kunt ook zien dat de foutafhandeling aanzienlijk is vereenvoudigd, ook al gebruikten we On Error Resume Next
. We hoeven de foutcode niet meer op te zoeken, omdat we in de meeste gevallen alleen geïnteresseerd zijn in of het gelukt is of niet. Wat nog belangrijker is, de foutafhandeling heeft de uitvoeringsstroom niet veranderd, waarbij u enige logica in de hoofdtekst en andere logica in de foutafhandeling hebt. Dit laatste is een situatie die we absoluut willen vermijden, want als er een fout is in de foutafhandelaar, kan het gedrag verrassend zijn. Het is het beste om te voorkomen dat dat een mogelijkheid is.
Het draait allemaal om abstractie
Maar de belangrijkste score die we hier behalen, is het abstractieniveau dat we nu kunnen bereiken. De originele versie van EditTableSubdatasheetProperty
bevat veel details op laag niveau over het DAO-object, gaat echt niet over het kerndoel van de functie. Denk aan dagen waarop u een procedure hebt gezien die honderden regels lang is met diep geneste lussen of voorwaarden. Zou je dat willen debuggen? Ik niet.
Dus als ik een procedure zie, is het eerste wat ik echt wil doen, de onderdelen eruit halen in hun eigen functie, zodat ik het abstractieniveau voor die procedure kan verhogen. Door onszelf te dwingen het abstractieniveau te verhogen, kunnen we ook grote klassen van bugs vermijden waarvan de oorzaak is dat een verandering in een deel van de megaprocedure onbedoelde gevolgen heeft voor de andere delen van de procedures. Wanneer we functies aanroepen en parameters doorgeven, verkleinen we ook de mogelijkheid dat ongewenste neveneffecten onze logica verstoren.
Daarom ben ik dol op het patroon "Try*". Ik hoop dat je het ook nuttig vindt voor je projecten.