sql >> Database teknologi >  >> RDS >> Access

Skrivning af læsbar kode til VBA – Prøv* mønster

Skrivning af læsbar kode til VBA – Prøv* mønster

På det seneste har jeg fundet mig selv i at bruge Try mønster mere og mere. Jeg kan virkelig godt lide dette mønster, fordi det giver meget mere læsbar kode. Dette er især vigtigt ved programmering i et modent programmeringssprog som VBA, hvor fejlhåndteringen er sammenflettet med kontrolflowet. Generelt synes jeg, at alle procedurer, der er afhængige af fejlhåndtering som et kontrolflow, er sværere at følge.

Scenarie

Lad os starte med et eksempel. DAO objektmodel er en perfekt kandidat på grund af, hvordan den fungerer. Se, alle DAO-objekter har Properties samling, som indeholder Property genstande. Alle kan dog tilføje tilpasset egenskab. Faktisk vil Access tilføje flere egenskaber til forskellige DAO-objekter. Derfor kan vi have en ejendom, der måske ikke eksisterer, og som skal håndtere både sagen om ændring af en eksisterende ejendoms værdi og sagen om tilføjelse af en ny ejendom.

Lad os bruge Subdatasheet ejendom som eksempel. Som standard vil alle tabeller, der er oprettet via Access UI, have egenskaben indstillet til Auto , men det vil vi måske ikke. Men hvis vi har tabeller, der er oprettet i kode eller på anden måde, har det muligvis ikke egenskaben. Så vi kan starte med en indledende version af koden for at opdatere alle tabellers egenskaber og håndtere begge sager.

Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="Subdatasheet GoToName"r On Erandrorler GoToName Indstil db =CurrentDb For hver tdf I db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Ikke tdf.Name Som "~*") Derefter 'Not added, or temp . Indstil 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 =tdfSheetPertyProName,(CreateDatpertyProperty.Name) dbText, NewValue) tdf.Properties.Append prp Fortsæt Fortsæt End If MsgBox Err.Number &":" &Err.Description Resume ExitProc End Sub

Koden vil sandsynligvis fungere. Men for at forstå det, er vi nok nødt til at tegne et eller andet flowdiagram. Linjen Set prp = tdf.Properties(SubDatasheetPropertyName) potentielt kunne give en fejl 3270. I dette tilfælde springer kontrollen til fejlhåndteringssektionen. Vi opretter derefter en egenskab og genoptager derefter på et andet sted i løkken ved hjælp af etiketten Continue . Der er nogle spørgsmål...

  • Hvad hvis 3270 hæves på en anden linje?
  • Antag, at linjen Set prp =... kaster ikke fejl 3270, men faktisk en anden fejl?
  • Hvad hvis, mens vi er inde i fejlbehandleren, sker der en anden fejl, når du udfører Append eller CreateProperty ?
  • Skal denne funktion overhovedet vise en Msgbox ? Tænk på funktioner, der formodes at virke på noget på vegne af formularer eller knapper. Hvis funktionerne viser en meddelelsesboks, og derefter afslutte normalt, har opkaldskoden ingen idé om, at noget er gået galt og kan fortsætte med at gøre ting, den ikke burde gøre.
  • Kan du se på koden og forstå, hvad den gør med det samme? Jeg kan ikke. Jeg er nødt til at skele til det, så tænke over, hvad der skal ske i tilfælde af en fejl og mentalt skitsere vejen. Det er ikke let at læse.

Tilføj en HasProperty procedure

Kan vi gøre det bedre? Ja! Nogle programmører genkender allerede problemet med at bruge fejlhåndtering, som jeg illustrerede, og har klogt abstraheret dette til sin egen funktion. Her er en bedre version:

Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="Subdatasheet CurrentName"D Set db For hver tdf I db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Og (Ikke tdf.Name Som "~*") Så 'Ikke vedhæftet, eller temp. If Not HasProperty(tdf, SubDatasheetPropertyName) Then Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyValName) Så New(SubDatasheetPropertyValName) End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Ignoreret som Variant Ved fejl Genoptag Næste Ignoreret =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End Function før> 

I stedet for at blande udførelsesflowet med fejlhåndteringen, har vi nu en funktion HasFunction som pænt abstraherer den fejltilbøjelige kontrol for en egenskab, der måske ikke eksisterer. Som en konsekvens har vi ikke brug for kompleks fejlhåndtering/udførelsesflow, som vi så i det første eksempel. Dette er en stor forbedring og giver en noget læsbar kode. Men...

  • Vi har én gren, der bruger variablen prp og vi har en anden gren, der bruger tdf.Properties(SubDatasheetPropertyName) der i virkeligheden refererer til den samme ejendom. Hvorfor gentager vi os selv med to forskellige måder at referere til den samme egenskab på?
  • Vi håndterer ejendommen ret meget. HasProperty skal håndtere egenskaben for at finde ud af, om den eksisterer, og returnerer derefter blot en Boolean resultat, og lader det være op til kaldekoden igen at prøve at hente den samme egenskab igen for at ændre værdien.
  • På samme måde håndterer vi NewValue mere end nødvendigt. Vi sender det enten i CreateProperty eller indstil Value ejendommens ejendom.
  • HasProperty funktion antager implicit, at objektet har en Properties medlem og kalder det sent bundet, hvilket betyder, at det er en runtime-fejl, hvis en forkert slags objekt leveres til det.

Brug TryGetProperty i stedet

Kan vi gøre det bedre? Ja! Det er her, vi skal se på Try-mønsteret. Hvis du nogensinde har programmeret med .NET, har du sandsynligvis set metoder som TryParse hvor vi i stedet for at rejse en fejl ved fiasko, kan opsætte en betingelse for at gøre noget for succes og noget andet for fiasko. Men endnu vigtigere, vi har resultatet tilgængeligt for succes. Så hvordan ville vi forbedre HasProperty fungere? For det første bør vi returnere Property objekt. Lad os prøve denne kode:

Public Function TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName As String, _ ByRef OutProperty As DAO.Property _) As Boolean On Error Resume Next Set OutProperty =SourceProperties(PropertyName) Then If Err.Numberty =Intet Slut Hvis Ved Fejl Gå til 0 TryGetProperty =(Ikke OutProperty Er Intet) Afslut funktion

Med få ændringer har vi opnået få store sejre:

  • Adgangen til Properties er ikke længere sent bundet. Vi behøver ikke håbe på, at et objekt har en egenskab ved navn Properties og det er af DAO.Properties . Dette kan verificeres på kompileringstidspunktet.
  • I stedet for blot en Boolean resultat, kan vi også få den hentede Property objekt, men kun på succesen. Hvis vi fejler, vil OutProperty parameter vil være Nothing . Vi vil stadig bruge Boolean resultat for at hjælpe med opsætning af flowet, som du snart vil se.
  • Ved at navngive vores nye funktion med Try præfiks, indikerer vi, at dette garanteret ikke giver en fejl under normale driftsforhold. Vi kan naturligvis ikke forhindre fejl i hukommelsen eller noget i den retning, men på det tidspunkt har vi meget større problemer. Men under den normale driftstilstand har vi undgået at sammenfiltre vores fejlhåndtering med udførelsesflowet. Koden kan nu læses fra top til bund uden at hoppe frem eller tilbage.

Bemærk, at jeg efter konvention præfikser egenskaben "out" med Out . Det hjælper med at gøre det klart, at vi formodes at overføre variablen til funktionen uinitialiseret. Vi forventer også, at funktionen vil initialisere parameteren. Det vil være klart, når vi ser på opkaldskoden. Så lad os konfigurere opkaldskoden.

Revideret opkaldskode ved hjælp af TryGetProperty

Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="Subdatasheet CurrentName"D Set db For hver tdf I db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Og (Ikke tdf.Name Som "~*") Så 'Ikke vedhæftet, eller temp. Hvis TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then If prp.Value <> NewValue Then prp.Value =NewValue End If Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) If EndAppProperties If EndAppProperties. Hvis NextEnd Sub

Koden er nu lidt mere læsbar med det første Prøv-mønster. Vi har formået at reducere håndteringen af ​​prp . Bemærk, at vi sender prp variabel i true , prp vil blive initialiseret med den egenskab, vi ønsker at manipulere. Ellers er prp forbliver Nothing . Vi kan derefter bruge CreateProperty for at initialisere prp variabel.

Vi vendte også negationen, så koden bliver lettere at læse. Vi har dog ikke rigtig reduceret håndteringen af ​​NewValue parameter. Vi har stadig en anden indlejret blok til at kontrollere værdien. Kan vi gøre det bedre? Ja! Lad os tilføje en anden funktion:

Tilføjelse af TrySetPropertyValue procedure

Offentlig funktion TrySetPropertyValue( _ ByVal SourceProperty As DAO.Property, _ ByVal NewValue As Variant_) As Boolean If SourceProperty.Value =PropertyValue Then TrySetPropertyValue =True Else on Error Resume Next Source ErueProperty =TrySetPropertyValue. SourceProperty.Value =NewValue) Afslut IfEnd-funktionen

Fordi vi garanterer, at denne funktion ikke giver en fejl, når værdien ændres, kalder vi den TrySetPropertyValue . Endnu vigtigere, denne funktion hjælper med at indkapsle alle de blodige detaljer omkring ændring af ejendommens værdi. Vi har en måde at garantere, at værdien er den værdi, vi forventede den ville være. Lad os se på, hvordan opkaldskoden vil blive ændret med denne funktion.

Opdateret opkaldskode ved hjælp af både TryGetProperty og TrySetPropertyValue

Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="Subdatasheet CurrentName"D Set db For hver tdf I db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Og (Ikke tdf.Name Som "~*") Så 'Ikke vedhæftet, eller temp. Hvis TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append

End Ifpre end If End If End If End If End If End If

Vi har fjernet en hel If blok. Vi kan nu blot læse koden og straks, at vi forsøger at indstille en ejendomsværdi, og hvis noget går galt, fortsætter vi bare. Det er meget nemmere at læse, og navnet på funktionen er selvbeskrivende. Et godt navn gør det mindre nødvendigt at slå definitionen af ​​funktionen op for at forstå, hvad den laver.

Opretter TryCreateOrSetProperty procedure

Koden er mere læsbar, men vi har stadig den Else blokere for at oprette en ejendom. Kan vi gøre det endnu bedre? Ja! Lad os tænke over, hvad vi skal opnå her. Vi har en ejendom, der måske eksisterer eller ikke. Hvis det ikke gør det, vil vi gerne skabe det. Uanset om det allerede eksisterede eller ej, har vi brug for, at det er sat til en bestemt værdi. Så det, vi har brug for, er en funktion, der enten opretter en ejendom eller opdaterer værdien, hvis den allerede eksisterer. For at oprette en ejendom skal vi kalde CreateProperty som desværre ikke er på Properties men snarere forskellige DAO-objekter. Derfor skal vi binde sent ved at bruge Object datatype. Vi kan dog stadig levere nogle runtime-tjek for at undgå fejl. Lad os oprette en TryCreateOrSetProperty funktion:

Public Function TryCreateOrSetProperty( _ ByVal SourceDaoObject As Object, _ ByVal PropertyName As String, _ ByVal PropertyType As DAO.DataTypeEnum, _ ByVal PropertyValue As Variant, _ ByRef OutProperty As CaseO.Property As DAO.Property Er DAO.TableDef, _ TypeOf SourceDaoObject Er DAO.QueryDef, _ TypeOf SourceDaoObject Er DAO.Field, _ TypeOf SourceDaoObject Er DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertyOrName)TryetOperuealS,TryetProperueS,Ejendom,EjendomEllerNavn,TryetProperueS Error Resume Next Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number. Indstil derefter OutProperty =Ingenting End If On Error GoTo 0 TryCreateOrSetProperty =(OutProperty Is Nothing) End If Case Else Err.Raise 5, , "Ugyldigt objekt angivet til SourceDaoObject-parameteren. Det skal være et DAO-objekt, der indeholder et CreateProperty-medlem." End SelectEnd Function

Et par ting at bemærke:

  • Vi var i stand til at bygge videre på den tidligere Try* funktion, vi definerede, som hjælper med at skære ned på kodningen af ​​funktionens krop, så den kan fokusere mere på oprettelsen, hvis der ikke er en sådan egenskab.
  • Dette er nødvendigvis mere udførligt på grund af de ekstra runtime-tjek, men vi er i stand til at sætte det op, så fejl ikke ændrer udførelsesflowet, og vi kan stadig læse fra top til bund uden at hoppe.
  • I stedet for at smide en MsgBox ud af ingenting bruger vi Err.Raise og returnere en meningsfuld fejl. Selve fejlhåndteringen delegeres til opkaldskoden, som derefter kan beslutte, om der skal vises en beskedboks til brugeren eller gøres noget andet.
  • På grund af vores omhyggelige håndtering og forudsat at SourceDaoObject parameteren er gyldig, garanterer alle mulige stier, at eventuelle problemer med at oprette eller indstille en eksisterende ejendoms værdi vil blive håndteret, og vi vil få en false resultat. Det påvirker opkaldskoden, som vi snart vil se.

Endelig version af opkaldskoden

Lad os opdatere opkaldskoden for at bruge den nye funktion:

Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="Subdatasheet CurrentName"D Set db For hver tdf I db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Og (Ikke tdf.Name Som "~*") Så 'Ikke vedhæftet, eller temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub

Det var noget af en forbedring af læsbarheden. I den originale version ville vi skulle granske en række If blokke og hvordan fejlhåndtering ændrer udførelsesflowet. Vi skulle finde ud af, hvad indholdet præcist gjorde for at konkludere, at vi forsøger at få en ejendom eller oprette den, hvis den ikke eksisterer, og få den sat til en bestemt værdi. Med den nuværende version er det hele der i navnet på funktionen, TryCreateOrSetProperty . Vi kan nu se, hvad funktionen forventes at gøre.

Konklusion

Du undrer dig måske, "men vi tilføjede meget flere funktioner og meget flere linjer. Er det ikke meget arbejde?” Det er rigtigt, at vi i denne nuværende version definerede yderligere 3 funktioner. Du kan dog læse hver enkelt funktion isoleret og stadig nemt forstå, hvad den skal gøre. Du så også, at TryCreateOrSetProperty funktion kunne bygge op på de 2 andre Try* funktioner. Det betyder, at vi har mere fleksibilitet til at samle logikken.

Så hvis vi skriver en anden funktion, der gør noget med egenskaben af ​​objekter, behøver vi ikke at skrive det hele over, og vi skal heller ikke kopiere og indsætte koden fra den originale EditTableSubdatasheetProperty ind i den nye funktion. Den nye funktion kan jo have brug for nogle forskellige varianter og dermed kræve en anden rækkefølge. Til sidst skal du huske på, at de rigtige modtagere er telefonkoden, der skal gøre noget. Vi ønsker at holde opkaldskoden på et ret højt niveau uden at blive bundet ind i detaljer, som kan være dårlige for vedligeholdelsen.

Du kan også se, at fejlhåndteringen er væsentligt forenklet, selvom vi brugte On Error Resume Next . Vi behøver ikke længere at slå fejlkoden op, for i størstedelen af ​​tilfældene er vi kun interesserede i, om det lykkedes eller ej. Endnu vigtigere er det, at fejlhåndteringen ikke ændrede udførelsesflowet, hvor du har noget logik i kroppen og anden logik i fejlhåndteringen. Sidstnævnte er en situation, vi absolut ønsker at undgå, for hvis der er en fejl i fejlbehandleren, så kan adfærden være overraskende. Det er bedst at undgå, at det er en mulighed.

Det handler om abstraktion

Men den vigtigste score, vi vinder her, er det abstraktionsniveau, vi nu kan opnå. Den originale version af EditTableSubdatasheetProperty indeholdt en masse detaljer på lavt niveau om DAO-objektet handler virkelig ikke om kernemålet med funktionen. Tænk på dage, hvor du har set en procedure, der er hundredvis af linjer lang med dybt indlejrede sløjfer eller forhold. Vil du fejlsøge det? Det gør jeg ikke.

Så når jeg ser en procedure, er den første ting, jeg virkelig vil gøre, at rive delene ud til deres egen funktion, så jeg kan hæve abstraktionsniveauet for den procedure. Ved at tvinge os selv til at skubbe abstraktionsniveauet kan vi også undgå store klasser af fejl, hvor årsagen er, at en ændring i en del af megaproceduren har utilsigtede konsekvenser for de andre dele af procedurerne. Når vi kalder funktioner og sender parametre, reducerer vi også muligheden for, at uønskede bivirkninger forstyrrer vores logik.

Derfor elsker jeg "Try*"-mønsteret. Jeg håber, at du også finder det nyttigt til dine projekter.


  1. Sådan fungerer FROM_BASE64()-funktionen i MySQL

  2. Få Hibernate og SQL Server til at spille godt med VARCHAR og NVARCHAR

  3. Kalendertabel for Data Warehouse

  4. Ikke mere SPU