Psaní čitelného kódu pro VBA – vzor Try*
V poslední době jsem zjistil, že používám Try
vzor stále více. Tento vzor se mi opravdu líbí, protože umožňuje mnohem čitelnější kód. To je zvláště důležité při programování ve vyspělém programovacím jazyce, jako je VBA, kde je zpracování chyb propojeno s tokem řízení. Obecně považuji za obtížnější sledovat jakékoli postupy, které se spoléhají na zpracování chyb jako řídicí tok.
Scénář
Začněme příkladem. Objektový model DAO je perfektním kandidátem kvůli tomu, jak funguje. Všechny objekty DAO mají Properties
kolekce, která obsahuje Property
objektů. Každý však může přidat vlastní vlastnost. Access ve skutečnosti přidá několik vlastností do různých objektů DAO. Proto můžeme mít vlastnost, která nemusí existovat, a musíme zvládnout jak případ změny hodnoty existující vlastnosti, tak případ připojení nové vlastnosti.
Použijme Subdatasheet
majetek jako příklad. Ve výchozím nastavení budou mít všechny tabulky vytvořené prostřednictvím uživatelského rozhraní Access vlastnost nastavenou na Auto
, ale to možná nechceme. Ale pokud máme tabulky, které jsou vytvořeny v kódu nebo jiným způsobem, nemusí mít tuto vlastnost. Můžeme tedy začít s počáteční verzí kódu, abychom aktualizovali vlastnosti všech tabulek a zvládli oba případy.
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 ="OnToSubdataName As Stringandler"OnToSubdataName Nastavit db =CurrentDb pro každý tdf v db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Ne tdf.Name jako "~*") Then 'Nepřipojeno nebo 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 prpdatePropertySme dbText, NewValue) tdf.Properties.Append prp Resume Continue End If MsgBox Err.Number &":" &Err.Description Resume ExitProc End Sub
Kód bude pravděpodobně fungovat. Abychom tomu však porozuměli, pravděpodobně budeme muset nakreslit nějaký vývojový diagram. Řádek Set prp = tdf.Properties(SubDatasheetPropertyName)
může potenciálně vyvolat chybu 3270. V tomto případě ovládací prvek skočí do sekce zpracování chyb. Poté vytvoříme vlastnost a poté pokračujeme v jiném bodě smyčky pomocí štítku Continue
. Existuje několik otázek…
- Co když je na nějakém jiném řádku zvýšeno 3270?
- Předpokládejme, že řádek
Set prp =...
nehází chyba 3270, ale ve skutečnosti nějaká jiná chyba? - Co když, když jsme uvnitř obslužné rutiny chyb, dojde k další chybě při provádění
Append
neboCreateProperty
? - Měla by tato funkce vůbec zobrazovat
Msgbox
? ? Přemýšlejte o funkcích, které by měly na něčem pracovat jménem formulářů nebo tlačítek. Pokud funkce zobrazí okno se zprávou a poté se normálně ukončí, volající kód netuší, že se něco pokazilo, a může pokračovat v činnostech, které by dělat neměl. - Můžete se podívat na kód a okamžitě pochopit, co dělá? nemohu. Musím na to mžourat, pak přemýšlet, co by se mělo stát v případě chyby, a v duchu načrtnout cestu. To není snadné číst.
Přidejte HasProperty
postup
Můžeme to udělat lépe? Ano! Někteří programátoři již znají problém s používáním zpracování chyb, jak jsem to ilustroval, a moudře to abstrahovali do své vlastní funkce. Zde je lepší verze:
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 ="Current Dubdatasheet As String" Pro každý tdf v db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Ne tdf.Name Jako "~*") Potom 'Nepřipojeno nebo temp. If Not HasProperty(tdf, SubDatasheetPropertyName) Then Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyValue) NewPropertyValue>NewPropertyValue) End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Ignored As Variant On Error Resume Next Ignored =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End FunctionMísto toho, abychom směšovali tok provádění se zpracováním chyb, máme nyní funkci
HasFunction
který úhledně abstrahuje kontrolu náchylnou k chybám na vlastnost, která nemusí existovat. V důsledku toho nepotřebujeme složité zpracování chyb / tok provádění, který jsme viděli v prvním příkladu. To je velké vylepšení a přispívá k poněkud čitelnému kódu. Ale…
- Máme jednu větev, která používá proměnnou
prp
a máme další větev, která používátdf.Properties(SubDatasheetPropertyName)
který ve skutečnosti odkazuje na stejnou vlastnost. Proč se opakujeme se dvěma různými způsoby odkazování na stejnou vlastnost? - Zabýváme se majetkem poměrně často.
HasProperty
musí vlastnost zpracovat, aby zjistil, zda existuje, a poté jednoduše vrátíBoolean
výsledkem je, že je ponecháno na volajícím kódu, aby se znovu pokusil získat stejnou vlastnost a změnil hodnotu. - Podobně zacházíme s
NewValue
více než je nutné. Buď jej předáme vCreateProperty
nebo nastavteValue
vlastnictví nemovitosti. - The
HasProperty
funkce implicitně předpokládá, že objekt máProperties
člen a nazývá jej late-bound, což znamená, že pokud je mu poskytnut nesprávný druh objektu, jedná se o běhovou chybu.
Použijte TryGetProperty
místo
Můžeme to udělat lépe? Ano! Zde se musíme podívat na vzor Try. Pokud jste někdy programovali s .NET, pravděpodobně jste viděli metody jako TryParse
kde místo vyvolání chyby při neúspěchu můžeme nastavit podmínku udělat něco pro úspěch a něco jiného pro neúspěch. Ale co je důležitější, máme výsledek k dispozici pro úspěch. Jak bychom tedy zlepšili HasProperty
funkce? Za prvé bychom měli vrátit Property
objekt. Zkusme tento kód:
Veřejná funkce TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName jako řetězec, _ ByRef OutProperty As DAO.Property _) As Boolean On Error Resume Next Set OutProperty =SourceProperties(PropertyName) Then Setr.Number =Nothing End If On Error Přejít na 0 TryGetProperty =(Not OutProperty Is Nothing)End Function
S několika změnami jsme zaznamenali několik velkých výher:
- Přístup k
Properties
již není se zpožděním. Nemusíme doufat, že objekt má vlastnost nazvanouProperties
a patří doDAO.Properties
. To lze ověřit v době kompilace. - Místo pouze
Boolean
výsledkem můžeme také získat načtenouProperty
objekt, ale pouze na úspěch. Pokud selžeme,OutProperty
parametr budeNothing
. Stále budeme používatBoolean
výsledek, který vám pomůže s nastavením toku, jak brzy uvidíte. - Pojmenováním naší nové funkce
Try
prefix, naznačujeme, že to za normálních provozních podmínek zaručeně nevyvolá chybu. Je zřejmé, že nemůžeme zabránit chybám z nedostatku paměti nebo něčemu takovému, ale v tu chvíli máme mnohem větší problémy. Ale za normálních provozních podmínek jsme se vyhnuli zamotání našeho zpracování chyb s průběhem provádění. Kód lze nyní číst shora dolů, aniž byste museli skákat dopředu nebo dozadu.
Všimněte si, že podle konvence přidávám před vlastnost „out“ předponu Out
. To pomáhá objasnit, že se předpokládá, že proměnnou předáme funkci neinicializovanou. Očekáváme také, že funkce inicializuje parametr. To bude jasné, když se podíváme na volací kód. Pojďme tedy nastavit volací kód.
Upravený volací kód pomocí 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 ="Current Dubdatasheet As String" Pro každý tdf v db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Ne tdf.Name Jako "~*") Potom 'Nepřipojeno nebo temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then If prp.Value <> NewValue Then prp.Value =NewValue End If Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbtdfper EndAppendText, NewValue EndAppendText) Pokud NextEnd Sub
Kód je nyní s prvním vzorem Try o něco čitelnější. Podařilo se nám omezit zpracování prp
. Všimněte si, že předáváme prp
proměnné do prp
bude inicializováno vlastností, se kterou chceme manipulovat. Jinak prp
zůstane Nothing
. Poté můžeme použít CreateProperty
k inicializaci prp
proměnná.
Také jsme obrátili negaci, aby byl kód snadněji čitelný. Ve skutečnosti jsme však nesnížili práci s NewValue
parametr. Stále máme další vnořený blok pro kontrolu hodnoty. Můžeme to udělat lépe? Ano! Přidejme další funkci:
Přidání TrySetPropertyValue
postup
Veřejná funkce TrySetPropertyValue( _ ByVal SourceProperty As DAO.Property, _ ByVal NewValue As Variant_) As Boolean If SourceProperty.Value =PropertyValue Then TrySetPropertyValue =True Else On Chyba Pokračování On New TryeProeee Resume On NewVoral SourceProper SourceProperty.Value =NewValue) End IfEnd Function
Protože garantujeme, že tato funkce nevyvolá chybu při změně hodnoty, nazýváme ji TrySetPropertyValue
. Ještě důležitější je, že tato funkce pomáhá zapouzdřit všechny krvavé detaily kolem změny hodnoty nemovitosti. Máme způsob, jak zaručit, že hodnota je taková, jakou jsme očekávali. Podívejme se, jak se pomocí této funkce změní volací kód.
Aktualizován volací kód pomocí TryGetProperty
a 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 ="Current Dubdatasheet As String" Pro každý tdf v db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Ne tdf.Name Jako "~*") Potom 'Nepřipojeno nebo temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties>Append Next End Ifpre End IfpreOdstranili jsme celý
If
blok. Nyní můžeme jednoduše přečíst kód a okamžitě, že se snažíme nastavit hodnotu vlastnosti a pokud se něco pokazí, prostě pokračujeme dál. To se čte mnohem snadněji a název funkce se popisuje sám. Dobré jméno snižuje nutnost vyhledávat definici funkce, abyste pochopili, co dělá.Vytvoření
TryCreateOrSetProperty
postupKód je čitelnější, ale stále máme to
Else
blok vytváření vlastnosti. Můžeme se ještě zlepšit? Ano! Zamysleme se nad tím, co zde musíme splnit. Máme nemovitost, která může a nemusí existovat. Pokud ne, chceme ji vytvořit. Ať už existoval nebo ne, potřebujeme, aby byl nastaven na určitou hodnotu. Potřebujeme tedy funkci, která buď vytvoří vlastnost, nebo aktualizuje hodnotu, pokud již existuje. Chcete-li vytvořit vlastnost, musíme zavolatCreateProperty
který bohužel není veProperties
ale spíše různé DAO objekty. Proto se musíme pozdě svázat pomocíObject
datový typ. Stále však můžeme poskytnout určité kontroly za běhu, abychom se vyhnuli chybám. Pojďme vytvořitTryCreateOrSetProperty
funkce:Veřejná funkce TryCreateOrSetProperty( _ ByVal SourceDaoObject As Object, _ ByVal PropertyName as String, _ ByVal PropertyType As DAO.DataTypeEnum, _ ByVal PropertyValue As Variant, _ ByRef OutProperty As_O.SourceObProperty As_O.SourceBoProperty Je DAO.TableDef, _ TypeOf SourceDaoObject je DAO.QueryDef, _ TypeOf SourceDaoObject je DAO.Field, _ TypeOf SourceDaoObject je DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertyName, OutPropertyO) Chyba Pokračovat Další Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Then Set OutProperty =Nothing End If On Error GoTo 0 TryCreateOrSetProperty =(OutProperty Is Nothing) End If Case Else Err.Raise 5, , "Neplatný objekt poskytnutý parametru SourceDaoObject. Musí to být objekt DAO, který obsahuje člena CreateProperty." End SelectEnd FunctionNěkolik poznámek:
- Byli jsme schopni navázat na dřívější
Try*
funkce, kterou jsme definovali, což pomáhá omezit kódování těla funkce a umožňuje jí více se soustředit na vytváření v případě, že taková vlastnost neexistuje. - Toto je nutně podrobnější kvůli dalším kontrolám za běhu, ale jsme schopni to nastavit tak, aby chyby neměnily tok provádění a my stále mohli číst shora dolů bez přeskakování.
- Namísto vyvolání
MsgBox
z ničeho nic používámeErr.Raise
a vrátí smysluplnou chybu. Vlastní zpracování chyb je delegováno na volací kód, který se pak může rozhodnout, zda uživateli zobrazí schránku se zprávou, nebo udělá něco jiného. - Vzhledem k naší pečlivé manipulaci a zajištění toho, že
SourceDaoObject
parametr je platný, všechny možné cesty zaručují, že jakékoli problémy s vytvořením nebo nastavením hodnoty existující vlastnosti budou vyřešeny a dostanemefalse
výsledek. To má vliv na volací kód, jak brzy uvidíme.
Konečná verze volacího kódu
Pojďme aktualizovat volací kód, aby mohl používat novou funkci:
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 ="Current Dubdatasheet As String" Pro každý tdf v db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Ne tdf.Name Jako "~*") Potom 'Nepřipojeno nebo temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub
To bylo docela zlepšení v čitelnosti. V původní verzi bychom museli prozkoumat řadu If
bloky a jak zpracování chyb mění tok provádění. Museli bychom zjistit, co přesně obsah dělal, abychom dospěli k závěru, že se snažíme získat vlastnost nebo ji vytvořit, pokud neexistuje, a nastavit ji na určitou hodnotu. V aktuální verzi je to vše v názvu funkce, TryCreateOrSetProperty
. Nyní vidíme, co se od funkce očekává.
Závěr
Možná se divíte, „ale přidali jsme mnohem více funkcí a mnohem více řádků. Není to moc práce?" Je pravda, že v této aktuální verzi jsme definovali další 3 funkce. Každou jednotlivou funkci však můžete číst samostatně a přesto snadno pochopíte, co by měla dělat. Také jste viděli, že TryCreateOrSetProperty
funkce by mohla navázat na 2 další Try*
funkcí. To znamená, že máme větší flexibilitu při sestavování logiky.
Pokud tedy napíšeme jinou funkci, která dělá něco s vlastností objektů, nemusíme ji psát celou, ani nekopírujeme a nevkládáme kód z původního EditTableSubdatasheetProperty
do nové funkce. Koneckonců, nová funkce může potřebovat různé varianty, a tudíž vyžadovat jinou sekvenci. Nakonec mějte na paměti, že skutečnými příjemci jsou volací kód, který musí něco udělat. Chceme udržet volací kód na poměrně vysoké úrovni, aniž bychom se museli utápět v detailech, které by mohly být špatné pro údržbu.
Můžete také vidět, že zpracování chyb je výrazně zjednodušeno, i když jsme použili On Error Resume Next
. Již nemusíme hledat chybový kód, protože nás ve většině případů zajímá pouze to, zda se to povedlo nebo ne. Ještě důležitější je, že zpracování chyb nezměnilo tok provádění, kde máte nějakou logiku v těle a jinou logiku při zpracování chyb. To je situace, které se rozhodně chceme vyhnout, protože pokud dojde k chybě v obslužné rutině chyb, může být chování překvapivé. Nejlepší je zabránit tomu, aby to byla možnost.
Vše je o abstrakci
Ale nejdůležitější skóre, které zde vyhrajeme, je úroveň abstrakce, které nyní můžeme dosáhnout. Původní verze EditTableSubdatasheetProperty
obsahoval mnoho nízkoúrovňových podrobností o objektu DAO ve skutečnosti není o hlavním cíli funkce. Vzpomeňte si na dny, kdy jste viděli proceduru dlouhou stovky řádků s hluboce vnořenými smyčkami nebo podmínkami. Chtěli byste to odladit? já ne.
Takže když vidím proceduru, první věc, kterou opravdu chci udělat, je vytrhnout části do jejich vlastní funkce, abych mohl zvýšit úroveň abstrakce pro tuto proceduru. Tím, že se přinutíme posouvat úroveň abstrakce, se také můžeme vyhnout velkým třídám chyb, jejichž příčinou je, že jedna změna v části megaprocedury má nezamýšlené důsledky pro ostatní části procedur. Když voláme funkce a předáváme parametry, snižujeme také možnost nežádoucích vedlejších účinků zasahujících do naší logiky.
Proto miluji vzor „Try*“. Doufám, že to bude užitečné i pro vaše projekty.