Schreiben von lesbarem Code für VBA – Try*-Muster
In letzter Zeit habe ich festgestellt, dass ich Try
verwende Muster immer mehr. Ich mag dieses Muster wirklich, weil es für viel besser lesbaren Code sorgt. Dies ist besonders wichtig, wenn in einer ausgereiften Programmiersprache wie VBA programmiert wird, wo die Fehlerbehandlung mit dem Kontrollfluss verflochten ist. Im Allgemeinen finde ich alle Verfahren, die auf Fehlerbehandlung als Kontrollfluss angewiesen sind, schwieriger zu befolgen.
Szenario
Beginnen wir mit einem Beispiel. Das DAO-Objektmodell ist aufgrund seiner Funktionsweise ein perfekter Kandidat. Sehen Sie, alle DAO-Objekte haben Properties
Sammlung, die Property
enthält Objekte. Jeder kann jedoch benutzerdefinierte Eigenschaften hinzufügen. Tatsächlich fügt Access verschiedenen DAO-Objekten mehrere Eigenschaften hinzu. Daher haben wir möglicherweise eine Eigenschaft, die möglicherweise nicht vorhanden ist, und müssen sowohl den Fall der Änderung des Werts einer vorhandenen Eigenschaft als auch den Fall des Anhängens einer neuen Eigenschaft behandeln.
Verwenden wir Subdatasheet
Eigentum als Beispiel. Standardmäßig ist die Eigenschaft aller über die Access-Benutzeroberfläche erstellten Tabellen auf Auto
festgelegt , aber das wollen wir vielleicht nicht. Wenn wir jedoch Tabellen haben, die im Code oder auf andere Weise erstellt wurden, verfügt sie möglicherweise nicht über die Eigenschaft. Wir können also mit einer ersten Version des Codes beginnen, um die Eigenschaften aller Tabellen zu aktualisieren und beide Fälle zu behandeln.
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 ="SubdatasheetName" On Error GoTo ErrHandler Setze db =CurrentDb für jede tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Not connected, 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.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Resume Continue End If MsgBox Err.Number &":" &Err.Description Resume ExitProc End Sub
Der Code wird wahrscheinlich funktionieren. Um es jedoch zu verstehen, müssen wir wahrscheinlich ein Flussdiagramm zeichnen. Die Zeile Set prp = tdf.Properties(SubDatasheetPropertyName)
könnte möglicherweise einen Fehler 3270 auslösen. In diesem Fall springt die Steuerung zum Fehlerbehandlungsabschnitt. Wir erstellen dann eine Eigenschaft und fahren dann an einer anderen Stelle der Schleife fort, indem wir das Label Continue
verwenden . Es gibt einige Fragen…
- Was ist, wenn 3270 in einer anderen Zeile ausgelöst wird?
- Angenommen, die Zeile
Set prp =...
wirft nicht Fehler 3270, aber eigentlich ein anderer Fehler? - Was ist, wenn, während wir uns in der Fehlerbehandlung befinden, beim Ausführen des
Append
ein weiterer Fehler auftritt oderCreateProperty
? - Sollte diese Funktion überhaupt eine
Msgbox
anzeigen ? Denken Sie an Funktionen, die im Auftrag von Formularen oder Schaltflächen an etwas arbeiten sollen. Wenn die Funktionen ein Meldungsfeld anzeigen und normal beendet werden, hat der aufrufende Code keine Ahnung, dass etwas schief gelaufen ist, und tut möglicherweise weiterhin Dinge, die er nicht tun sollte. - Können Sie einen Blick auf den Code werfen und sofort verstehen, was er tut? Ich kann nicht. Ich muss darauf blinzeln, dann überlegen, was im Fehlerfall passieren soll und den Weg gedanklich skizzieren. Das ist nicht leicht zu lesen.
Fügen Sie eine HasProperty
hinzu Verfahren
Können wir es besser machen? Ja! Einige Programmierer haben das Problem mit der Fehlerbehandlung, wie ich sie illustriert habe, bereits erkannt und klugerweise in eine eigene Funktion abstrahiert. Hier ist eine bessere 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 ="SubdatasheetName" Set db =CurrentDb Für jeden tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Not connected, or temp. If Not HasProperty(tdf, SubDatasheetPropertyName) Then Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyName) <> NewValue Then tdf.Properties(SubDatasheetPropertyName) =NewValue End If 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 FunctionAnstatt den Ausführungsablauf mit der Fehlerbehandlung zu verwechseln, haben wir jetzt eine Funktion
HasFunction
wodurch die fehleranfällige Überprüfung auf eine Eigenschaft, die möglicherweise nicht vorhanden ist, sauber abstrahiert wird. Infolgedessen benötigen wir keinen komplexen Fehlerbehandlungs-/Ausführungsfluss, den wir im ersten Beispiel gesehen haben. Dies ist eine große Verbesserung und sorgt für einigermaßen lesbaren Code. Aber…
- Wir haben einen Zweig, der die Variable
prp
verwendet und wir haben einen weiteren Zweig, dertdf.Properties(SubDatasheetPropertyName)
verwendet das sich tatsächlich auf die gleiche Eigenschaft bezieht. Warum wiederholen wir uns mit zwei verschiedenen Möglichkeiten, auf dieselbe Eigenschaft zu verweisen? - Wir kümmern uns ziemlich viel um das Eigentum. Die
HasProperty
muss die Eigenschaft handhaben, um herauszufinden, ob sie existiert, und gibt dann einfach einenBoolean
zurück Ergebnis und überlässt es dem aufrufenden Code, erneut zu versuchen, dieselbe Eigenschaft erneut abzurufen, um den Wert zu ändern. - Ähnlich handhaben wir den
NewValue
mehr als nötig. Wir übergeben es entweder inCreateProperty
oder setzen Sie denValue
Eigentum des Eigentums. - Die
HasProperty
Funktion nimmt implizit an, dass das Objekt eineProperties
hat Member und nennt es spät gebunden, was bedeutet, dass es ein Laufzeitfehler ist, wenn ihm eine falsche Art von Objekt bereitgestellt wird.
Verwenden Sie TryGetProperty
stattdessen
Können wir es besser machen? Ja! Hier müssen wir uns das Try-Muster ansehen. Wenn Sie jemals mit .NET programmiert haben, haben Sie wahrscheinlich Methoden wie TryParse
gesehen Anstatt bei einem Fehler einen Fehler zu melden, können wir eine Bedingung aufstellen, um etwas für den Erfolg und etwas anderes für den Fehler zu tun. Aber noch wichtiger ist, dass wir das Ergebnis für den Erfolg zur Verfügung haben. Wie würden wir also die HasProperty
verbessern? Funktion? Zum einen sollten wir die Property
zurückgeben Objekt. Versuchen wir diesen Code:
Öffentliche Funktion 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) If Err.Number Then Set OutProperty =Nichts End If On Error GoTo 0 TryGetProperty =(Not OutProperty is Nothing)Funktion beenden
Mit wenigen Änderungen haben wir einige große Gewinne erzielt:
- Der Zugriff auf
Properties
ist nicht mehr verspätet. Wir müssen nicht darauf hoffen, dass ein Objekt eine Eigenschaft namensProperties
hat und es ist vonDAO.Properties
. Dies kann zur Kompilierzeit überprüft werden. - Statt nur einem
Boolean
Als Ergebnis können wir auch die abgerufeneProperty
erhalten Objekt, sondern nur auf den Erfolg. Wenn wir scheitern, wird dieOutProperty
Parameter istNothing
. Wir werden weiterhin denBoolean
verwenden Ergebnis, um beim Einrichten des Flusses zu helfen, wie Sie in Kürze sehen werden. - Indem Sie unsere neue Funktion mit
Try
benennen Präfix, weisen wir darauf hin, dass dies unter normalen Betriebsbedingungen garantiert keinen Fehler auslöst. Natürlich können wir Speichermangel oder ähnliches nicht verhindern, aber an diesem Punkt haben wir viel größere Probleme. Aber unter normalen Betriebsbedingungen haben wir es vermieden, unsere Fehlerbehandlung mit dem Ausführungsfluss zu verwechseln. Der Code kann nun ohne Vor- und Zurückspringen von oben nach unten gelesen werden.
Beachten Sie, dass ich der „out“-Eigenschaft per Konvention das Präfix Out
voranstelle . Das hilft zu verdeutlichen, dass wir die Variable uninitialisiert an die Funktion übergeben sollen. Wir erwarten auch, dass die Funktion den Parameter initialisiert. Das wird klar, wenn wir uns den aufrufenden Code ansehen. Lassen Sie uns also den Anrufcode einrichten.
Überarbeiteter Aufrufcode mit 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 ="SubdatasheetName" Set db =CurrentDb Für jeden tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Not connected, or 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.Properties.Append prp End If End If End Wenn NextEnd Sub
Der Code ist jetzt mit dem ersten Try-Muster etwas besser lesbar. Wir haben es geschafft, die Handhabung des prp
zu reduzieren . Beachten Sie, dass wir den prp
übergeben Variable in die prp
wird mit der Eigenschaft initialisiert, die wir manipulieren möchten. Ansonsten die prp
bleibt Nothing
. Wir können dann die CreateProperty
verwenden um das prp
zu initialisieren Variable.
Wir haben auch die Negation umgedreht, damit der Code leichter lesbar wird. Allerdings haben wir die Handhabung von NewValue
nicht wirklich reduziert Parameter. Wir haben noch einen weiteren verschachtelten Block, um den Wert zu überprüfen. Können wir es besser machen? Ja! Lassen Sie uns eine weitere Funktion hinzufügen:
Hinzufügen von TrySetPropertyValue
Verfahren
Öffentliche 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 SourceProperty.Value =NewValue On Error GoTo 0 TrySetPropertyValue =( SourceProperty.Value =NewValue) Beenden Sie die IfEnd-Funktion
Da wir garantieren, dass diese Funktion beim Ändern des Werts keinen Fehler auslöst, nennen wir sie TrySetPropertyValue
. Noch wichtiger ist, dass diese Funktion dabei hilft, alle blutigen Details im Zusammenhang mit der Wertänderung der Immobilie zu kapseln. Wir haben eine Möglichkeit zu garantieren, dass der Wert der Wert ist, den wir erwartet haben. Schauen wir uns an, wie der aufrufende Code mit dieser Funktion geändert wird.
Aufrufcode aktualisiert, der sowohl TryGetProperty
verwendet und 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 ="SubdatasheetName" Set db =CurrentDb Für jeden tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Not connected, or temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp End If End If End If NextEnd Sub
Wir haben ein komplettes If
eliminiert Block. Wir können jetzt einfach den Code lesen und sofort versuchen, einen Eigenschaftswert festzulegen, und wenn etwas schief geht, machen wir einfach weiter. Das ist viel einfacher zu lesen und der Name der Funktion ist selbsterklärend. Ein guter Name macht es weniger notwendig, die Definition der Funktion nachzuschlagen, um zu verstehen, was sie tut.
Erstellen von TryCreateOrSetProperty
Verfahren
Der Code ist besser lesbar, aber wir haben immer noch diesen Else
Blockieren Sie das Erstellen einer Eigenschaft. Können wir es noch besser machen? Ja! Lassen Sie uns darüber nachdenken, was wir hier erreichen müssen. Wir haben eine Immobilie, die existieren kann oder nicht. Wenn nicht, wollen wir es schaffen. Unabhängig davon, ob es bereits existierte oder nicht, wir müssen es auf einen bestimmten Wert setzen. Was wir also brauchen, ist eine Funktion, die entweder eine Eigenschaft erstellt oder den Wert aktualisiert, falls er bereits vorhanden ist. Um eine Eigenschaft zu erstellen, müssen wir CreateProperty
aufrufen was leider nicht auf den Properties
steht sondern unterschiedliche DAO-Objekte. Daher müssen wir spät binden, indem wir Object
verwenden Datentyp. Wir können jedoch einige Laufzeitprüfungen bereitstellen, um Fehler zu vermeiden. Lassen Sie uns eine TryCreateOrSetProperty
erstellen Funktion:
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 _) As Boolean Select Case True Case TypeOf SourceDaoObject Ist DAO.TableDef, _ TypeOf SourceDaoObject Ist DAO.QueryDef, _ TypeOf SourceDaoObject Ist DAO.Field, _ TypeOf SourceDaoObject Ist DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertyName, OutProperty) Then TryCreateOrSetProperty =TrySetPropertyValue(OutProperty, PropertyValue) Else On Fehler fortsetzen Weiter 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, , „Ungültiges Objekt für den SourceDaoObject-Parameter bereitgestellt. Es muss ein DAO-Objekt sein, das ein CreateProperty-Member enthält." End SelectEnd Function
Einige Dinge zu beachten:
- Wir konnten auf dem früheren
Try*
aufbauen Funktion, die wir definiert haben, was dazu beiträgt, die Codierung des Hauptteils der Funktion zu reduzieren, sodass sie sich mehr auf die Erstellung konzentrieren kann, falls es keine solche Eigenschaft gibt. - Dies ist aufgrund der zusätzlichen Laufzeitprüfungen notwendigerweise ausführlicher, aber wir können es so einrichten, dass Fehler den Ausführungsfluss nicht verändern und wir immer noch ohne Sprünge von oben nach unten lesen können.
- Anstatt eine
MsgBox
zu werfen aus dem Nichts verwenden wirErr.Raise
und einen sinnvollen Fehler zurückgeben. Die eigentliche Fehlerbehandlung wird an den aufrufenden Code delegiert, der dann entscheiden kann, ob er dem Benutzer eine Messagebox anzeigt oder etwas anderes tut. - Aufgrund unserer sorgfältigen Handhabung und Bereitstellung des
SourceDaoObject
Parameter gültig ist, garantieren alle möglichen Pfade, dass alle Probleme beim Erstellen oder Festlegen des Werts einer vorhandenen Eigenschaft behandelt werden und wir einenfalse
erhalten Ergebnis. Das wirkt sich auf den aufrufenden Code aus, wie wir gleich sehen werden.
Endgültige Version des Aufrufcodes
Lassen Sie uns den aufrufenden Code aktualisieren, um die neue Funktion zu verwenden:
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 ="SubdatasheetName" Set db =CurrentDb Für jeden tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Not connected, or temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub
Das war eine ziemliche Verbesserung der Lesbarkeit. In der Originalversion müssten wir über eine Reihe von If
hinwegsehen Blöcke und wie die Fehlerbehandlung den Ausführungsablauf verändert. Wir müssten herausfinden, was genau der Inhalt getan hat, um zu dem Schluss zu kommen, dass wir versuchen, eine Eigenschaft zu erhalten oder zu erstellen, wenn sie nicht existiert, und sie auf einen bestimmten Wert setzen zu lassen. In der aktuellen Version steckt alles im Namen der Funktion, TryCreateOrSetProperty
. Wir können jetzt sehen, was die Funktion tun soll.
Schlussfolgerung
Sie fragen sich vielleicht, „aber wir haben viel mehr Funktionen und viel mehr Zeilen hinzugefügt. Ist das nicht viel Arbeit?“ Es stimmt, dass wir in dieser aktuellen Version 3 weitere Funktionen definiert haben. Sie können jedoch jede einzelne Funktion isoliert lesen und trotzdem leicht verstehen, was sie tun soll. Sie haben auch gesehen, dass TryCreateOrSetProperty
Funktion könnte auf den 2 anderen Try*
aufbauen Funktionen. Das bedeutet, dass wir mehr Flexibilität bei der Zusammenstellung der Logik haben.
Wenn wir also eine andere Funktion schreiben, die etwas mit der Eigenschaft von Objekten macht, müssen wir sie nicht komplett neu schreiben, noch müssen wir den Code aus der ursprünglichen EditTableSubdatasheetProperty
kopieren und einfügen in die neue Funktion. Schließlich benötigt die neue Funktion möglicherweise einige andere Varianten und damit eine andere Reihenfolge. Denken Sie schließlich daran, dass die wirklichen Nutznießer der aufrufende Code ist, der etwas tun muss. Wir möchten den aufrufenden Code auf einem relativ hohen Niveau halten, ohne in Details verstrickt zu sein, die sich nachteilig auf die Wartung auswirken können.
Sie können auch sehen, dass die Fehlerbehandlung erheblich vereinfacht wurde, obwohl wir On Error Resume Next
verwendet haben . Wir müssen den Fehlercode nicht mehr nachschlagen, da uns in den meisten Fällen nur interessiert, ob es gelungen ist oder nicht. Noch wichtiger ist, dass die Fehlerbehandlung den Ausführungsablauf nicht geändert hat, bei dem Sie einige Logik im Hauptteil und andere Logik in der Fehlerbehandlung haben. Letzteres ist eine Situation, die wir auf jeden Fall vermeiden wollen, denn wenn ein Fehler im Fehlerhandler auftritt, kann das Verhalten überraschend sein. Es ist am besten, dies zu vermeiden.
Es dreht sich alles um Abstraktion
Aber die wichtigste Punktzahl, die wir hier gewinnen, ist das Abstraktionsniveau, das wir jetzt erreichen können. Die ursprüngliche Version von EditTableSubdatasheetProperty
viele Low-Level-Details über das DAO-Objekt enthielt, geht es wirklich nicht um das Kernziel der Funktion. Denken Sie an Tage, an denen Sie eine Prozedur gesehen haben, die Hunderte von Zeilen lang ist und tief verschachtelte Schleifen oder Bedingungen enthält. Möchten Sie das debuggen? Ich nicht.
Wenn ich also eine Prozedur sehe, möchte ich als Erstes die Teile in ihre eigene Funktion zerlegen, damit ich die Abstraktionsebene für diese Prozedur erhöhen kann. Indem wir uns dazu zwingen, die Abstraktionsebene zu erhöhen, können wir auch große Klassen von Fehlern vermeiden, bei denen die Ursache darin besteht, dass eine Änderung in einem Teil der Megaprozedur unbeabsichtigte Auswirkungen auf die anderen Teile der Prozeduren hat. Wenn wir Funktionen aufrufen und Parameter übergeben, reduzieren wir auch die Möglichkeit unerwünschter Nebeneffekte, die unsere Logik stören.
Deshalb liebe ich das „Try*“-Muster. Ich hoffe, Sie finden es auch für Ihre Projekte nützlich.