sql >> Databasteknik >  >> RDS >> Access

Skriver läsbar kod för VBA – Prova* mönster

Skriva läsbar kod för VBA – Prova* mönster

På senare tid har jag kommit på mig själv med att använda Try mönster mer och mer. Jag gillar verkligen det här mönstret eftersom det ger mycket mer läsbar kod. Detta är särskilt viktigt vid programmering i ett moget programmeringsspråk som VBA där felhanteringen är sammanflätad med kontrollflödet. Generellt sett tycker jag att alla procedurer som bygger på felhantering som ett kontrollflöde är svårare att följa.

Scenario

Låt oss börja med ett exempel. DAO objektmodell är en perfekt kandidat på grund av hur den fungerar. Se, alla DAO-objekt har Properties samling, som innehåller Property föremål. Däremot kan vem som helst lägga till anpassad egendom. Faktum är att Access lägger till flera egenskaper till olika DAO-objekt. Därför kan vi ha en egenskap som kanske inte finns och måste hantera både fallet med att ändra en befintlig fastighets värde och fallet med att lägga till en ny egenskap.

Låt oss använda Subdatasheet egendom som exempel. Som standard kommer alla tabeller som skapats via Access UI ha egenskapen inställd på Auto , men det kanske vi inte vill. Men om vi har tabeller som är skapade i kod eller på något annat sätt, kanske det inte har egenskapen. Så vi kan börja med en första version av koden för att uppdatera alla tabellers egenskaper och hantera båda fallen.

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 ="HUnderdatasheet GoToName" Err On Erandrorler GoToName Ställ in db =CurrentDb för varje tdf i db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Inte tdf.Name Som "~*") Sedan 'Inte ansluten, eller temp . Ange 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 =tdfSheetasPropertyName,(CreateDatpertyPropertyName. dbText, NewValue) tdf.Properties.Append prp Resume Fortsätt End If MsgBox Err.Number &":" &Err.Description Resume ExitProc End Sub

Koden kommer förmodligen att fungera. Men för att förstå det måste vi antagligen rita upp ett flödesschema. Raden Set prp = tdf.Properties(SubDatasheetPropertyName) skulle potentiellt kunna ge ett fel 3270. I detta fall hoppar kontrollen till felhanteringssektionen. Vi skapar sedan en egenskap och återupptar sedan vid en annan punkt i slingan med etiketten Continue . Det finns några frågor...

  • Vad händer om 3270 höjs på någon annan linje?
  • Anta att raden Set prp =... kastar inte fel 3270 men faktiskt något annat fel?
  • Tänk om medan vi är inne i felhanteraren, inträffar ett annat fel när du kör Append eller CreateProperty ?
  • Ska den här funktionen ens visa en Msgbox ? Tänk på funktioner som är tänkta att fungera på något på uppdrag av formulär eller knappar. Om funktionerna visar en meddelanderuta, avsluta normalt, anropskoden har ingen aning om att något har gått fel och kan fortsätta att göra saker som den inte borde göra.
  • Kan du titta på koden och förstå vad den gör direkt? Jag kan inte. Jag måste kisa åt det, sedan fundera på vad som ska hända under fallet med ett fel och mentalt skissa på vägen. Det är inte lätt att läsa.

Lägg till en HasProperty förfarande

Kan vi göra bättre? ja! Vissa programmerare känner redan igen problemet med att använda felhantering som jag illustrerat och har klokt abstraherat detta till sin egen funktion. Här är en bättre 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 db För varje tdf i db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Och (Inte tdf.Name Som "~*") Sedan 'Inte ansluten, eller temp. Om inte HasProperty(tdf, SubDatasheetPropertyName) Ställ sedan in prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyValueName) Then(SubDatasheetPropertyValName) <>Nytt EndPropertySheet.Data End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Ignoreras Som Variant Vid fel Återuppta Nästa Ignorerad =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End Function före> 

Istället för att blanda ihop exekveringsflödet med felhanteringen har vi nu en funktion HasFunction som prydligt abstraherar ut den felbenägna kontrollen för en egenskap som kanske inte existerar. Som en konsekvens behöver vi inte komplicerat felhantering/exekveringsflöde som vi såg i det första exemplet. Detta är en stor förbättring och ger något läsbar kod. Men...

  • Vi har en gren som använder variabeln prp och vi har en annan gren som använder tdf.Properties(SubDatasheetPropertyName) som i själva verket avser samma egendom. Varför upprepar vi oss själva med två olika sätt att referera till samma egenskap?
  • Vi hanterar fastigheten ganska mycket. HasProperty måste hantera egenskapen för att ta reda på om den finns och returnerar sedan helt enkelt en Boolean resultat, och lämna det upp till anropskoden att igen försöka få samma egenskap igen för att ändra värdet.
  • På liknande sätt hanterar vi NewValue mer än nödvändigt. Vi skickar det antingen i CreateProperty eller ställ in Value fastighetens egendom.
  • HasProperty funktionen förutsätter implicit att objektet har en Properties medlem och kallar det late-bound, vilket betyder att det är ett körtidsfel om en felaktig typ av objekt tillhandahålls till den.

Använd TryGetProperty istället

Kan vi göra bättre? ja! Det är där vi måste titta på Try-mönstret. Om du någonsin har programmerat med .NET har du förmodligen sett metoder som TryParse där vi istället för att ta upp ett fel vid misslyckande, kan ställa upp ett villkor för att göra något för att lyckas och något annat för att misslyckas. Men ännu viktigare, vi har resultatet tillgängligt för framgång. Så hur skulle vi förbättra HasProperty fungera? För det första bör vi returnera Property objekt. Låt oss prova den här koden:

Public Function TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName As String, _ ByRef OutProperty As DAO.Property _) Som Boolean Vid fel Återuppta Nästa Set OutProperty =SourceProperties(PropertyName) Then Setr.PropertyNumberty =Inget slut om vid fel GoTo 0 TryGetProperty =(Inte OutProperty är ingenting) Avsluta funktion

Med få ändringar har vi gjort några stora vinster:

  • Åtkomsten till Properties är inte längre sen bunden. Vi behöver inte hoppas att ett objekt har en egenskap som heter Properties och det är av DAO.Properties . Detta kan verifieras vid kompileringstillfället.
  • Istället för bara en Boolean resultat kan vi också få den hämtade Property objekt, men bara på framgången. Om vi ​​misslyckas visas OutProperty parametern kommer att vara Nothing . Vi kommer fortfarande att använda Boolean resultat för att hjälpa dig att ställa in flödet som du kommer att se inom kort.
  • Genom att namnge vår nya funktion med Try prefix indikerar vi att detta garanterat inte orsakar ett fel under normala driftsförhållanden. Uppenbarligen kan vi inte förhindra minnesfel eller något liknande, men vid den tidpunkten har vi mycket större problem. Men under normala driftsförhållanden har vi undvikit att trassla ihop vår felhantering med exekveringsflödet. Koden kan nu läsas uppifrån och ner utan att hoppa fram eller tillbaka.

Observera att jag enligt konvention prefix egenskapen "out" med Out . Det hjälper till att göra det tydligt att vi ska skicka in variabeln till funktionen oinitierad. Vi förväntar oss också att funktionen initierar parametern. Det kommer att vara tydligt när vi tittar på anropskoden. Så låt oss ställa in samtalskoden.

Reviderad anropskod med 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 db För varje tdf i db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Och (Inte tdf.Name Som "~*") Sedan 'Inte ansluten, eller 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) tdf. EndAppendProperties. Om NextEnd Sub

Koden är nu lite mer läsbar med det första Try-mönstret. Vi har lyckats minska hanteringen av prp . Observera att vi skickar prp variabel till true , prp kommer att initieras med egenskapen vi vill manipulera. Annars används prp förblir Nothing . Vi kan sedan använda CreateProperty för att initiera prp variabel.

Vi vände även på negationen så att koden blir lättare att läsa. Vi har dock inte riktigt minskat hanteringen av NewValue parameter. Vi har fortfarande ett annat kapslat block för att kontrollera värdet. Kan vi göra bättre? ja! Låt oss lägga till ytterligare en funktion:

Lägger till TrySetPropertyValue förfarande

Public Function 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 ErrorProperty (FörsökValue) SourceProperty.Value =NewValue) Avsluta IfEnd-funktionen

Eftersom vi garanterar att den här funktionen inte ger ett fel när värdet ändras, kallar vi det TrySetPropertyValue . Ännu viktigare, den här funktionen hjälper till att kapsla in alla blodiga detaljer kring att ändra fastighetens värde. Vi har ett sätt att garantera att värdet är det värde vi förväntade oss att det skulle vara. Låt oss titta på hur samtalskoden kommer att ändras med den här funktionen.

Uppdaterad samtalskod med både TryGetProperty och 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 db För varje tdf i db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Och (Inte tdf.Name Som "~*") Sedan 'Inte ansluten, eller temp. Om TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp End If End Sub End If End If End 

Vi har tagit bort en hel If blockera. Vi kan nu helt enkelt läsa koden och omedelbart att vi försöker sätta ett fastighetsvärde och om något går fel fortsätter vi bara att gå vidare. Det är mycket lättare att läsa och namnet på funktionen är självbeskrivande. Ett bra namn gör det mindre nödvändigt att slå upp definitionen av funktionen för att förstå vad den gör.

Skapar TryCreateOrSetProperty förfarande

Koden är mer läsbar men vi har fortfarande den Else blockera att skapa en egenskap. Kan vi göra ännu bättre? ja! Låt oss fundera på vad vi behöver åstadkomma här. Vi har en fastighet som kanske inte finns. Om det inte gör det vill vi skapa det. Oavsett om det redan existerade eller inte, måste vi ställa in det på ett visst värde. Så det vi behöver är en funktion som antingen skapar en egenskap eller uppdaterar värdet om det redan finns. För att skapa en egenskap måste vi anropa CreateProperty som tyvärr inte finns på Properties utan snarare olika DAO-objekt. Därför måste vi binda sent genom att använda Object data typ. Men vi kan fortfarande tillhandahålla vissa körtidskontroller för att undvika fel. Låt oss skapa 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 Är DAO.TableDef, _ TypeOf SourceDaoObject Är DAO.QueryDef, _ TypeOf SourceDaoObject är DAO.Field, _ TypeOf SourceDaoObject är DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertyOrName)TryetOperuealS,TryetOperuealS Property Fel Resume Next Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Ställ sedan OutProperty =Ingenting End If On Error GoTo 0 TryCreateOrSetProperty =(OutProperty är ingenting) End If Case Else Err.Raise 5, , "Ogiltigt objekt tillhandahålls till parametern SourceDaoObject. Det måste vara ett DAO-objekt som innehåller en CreateProperty-medlem." Avsluta SelectEnd Function

Några saker att notera:

  • Vi kunde bygga vidare på den tidigare Try* funktion vi definierade, vilket hjälper till att minska kodningen av funktionens kropp, vilket gör att den kan fokusera mer på skapandet om det inte finns någon sådan egenskap.
  • Detta är nödvändigtvis mer utförligt på grund av de ytterligare körtidskontrollerna, men vi kan ställa in det så att fel inte ändrar exekveringsflödet och vi kan fortfarande läsa uppifrån och ner utan att hoppa.
  • Istället för att kasta en MsgBox från ingenstans använder vi Err.Raise och returnera ett meningsfullt fel. Själva felhanteringen delegeras till anropskoden som sedan kan bestämma om en meddelandelåda ska visas för användaren eller göra något annat.
  • På grund av vår försiktiga hantering och förutsatt att SourceDaoObject parametern är giltig, garanterar alla möjliga sökvägar att eventuella problem med att skapa eller ställa in en befintlig egenskaps värde kommer att hanteras och vi får en false resultat. Det påverkar anropskoden som vi kommer att se inom kort.

Slutlig version av anropskoden

Låt oss uppdatera anropskoden för att använda den nya funktionen:

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 db För varje tdf i db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 Och (Inte tdf.Name Som "~*") Sedan 'Inte ansluten, eller temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub

Det var en rejäl förbättring av läsbarheten. I den ursprungliga versionen skulle vi behöva granska ett antal If block och hur felhantering förändrar flödet av exekvering. Vi måste ta reda på exakt vad innehållet gjorde för att dra slutsatsen att vi försöker skaffa en fastighet eller skapa den om den inte finns och få den att ställas in på ett visst värde. Med den nuvarande versionen finns allt i funktionens namn, TryCreateOrSetProperty . Vi kan nu se vad funktionen förväntas göra.

Slutsats

Du kanske undrar, "men vi har lagt till mycket fler funktioner och mycket fler rader. Är det inte mycket jobb?" Det är sant att vi i den här nuvarande versionen definierade ytterligare 3 funktioner. Du kan dock läsa varje enskild funktion isolerat och ändå enkelt förstå vad den ska göra. Du såg också att TryCreateOrSetProperty funktionen kan byggas upp på de 2 andra Try* funktioner. Det betyder att vi har mer flexibilitet när det gäller att sätta ihop logiken.

Så om vi skriver en annan funktion som gör något med egenskapen hos objekt, behöver vi inte skriva över det hela och inte heller kopiera och klistra in koden från den ursprungliga EditTableSubdatasheetProperty in i den nya funktionen. När allt kommer omkring kan den nya funktionen behöva några olika varianter och därmed kräva en annan sekvens. Slutligen, kom ihåg att de verkliga förmånstagarna är anropskoden som behöver göra något. Vi vill hålla samtalskoden på en ganska hög nivå utan att fastna i detaljer som kan vara dåliga för underhållet.

Du kan också se att felhanteringen är avsevärt förenklad, även om vi använde On Error Resume Next . Vi behöver inte längre leta upp felkoden eftersom vi i majoriteten av fallen bara är intresserade av om det lyckades eller inte. Ännu viktigare är att felhanteringen inte förändrade exekveringsflödet där du har viss logik i kroppen och annan logik i felhanteringen. Det senare är en situation vi definitivt vill undvika eftersom om det finns ett fel i felhanteraren kan beteendet vara överraskande. Det är bäst att undvika att det är en möjlighet.

Allt handlar om abstraktion

Men den viktigaste poängen vi vinner här är den abstraktionsnivå vi nu kan uppnå. Den ursprungliga versionen av EditTableSubdatasheetProperty innehöll många detaljer på låg nivå om DAO-objektet handlar verkligen inte om funktionens kärnmål. Tänk på dagar då du har sett en procedur som är hundratals rader lång med djupt kapslade slingor eller förhållanden. Skulle du vilja felsöka det? Det gör jag inte.

Så när jag ser en procedur är det första jag verkligen vill göra att riva ut delarna till sin egen funktion, så att jag kan höja abstraktionsnivån för den proceduren. Genom att tvinga oss själva att tänja på abstraktionsnivån kan vi också undvika stora klasser av buggar där orsaken är att en förändring i en del av megaproceduren har oavsiktliga konsekvenser för de andra delarna av procedurerna. När vi anropar funktioner och skickar parametrar minskar vi också risken för oönskade biverkningar som stör vår logik.

Det är därför jag älskar mönstret "Try*". Jag hoppas att du också tycker att den är användbar för dina projekt.


  1. Hur man migrerar från Oracle DB till MariaDB

  2. 4 sätt att hitta rader som innehåller versaler i Oracle

  3. SQL mindre än eller lika med (=) operatör för nybörjare

  4. När spolar SQL Server-sorter tillbaka?