Access
 sql >> Datenbank >  >> RDS >> Access

Schreiben von lesbarem Code für VBA – Try*-Muster

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 oder CreateProperty ?
  • 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 Function 

Anstatt 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, der tdf.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 einen Boolean 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 in CreateProperty oder setzen Sie den Value Eigentum des Eigentums.
  • Die HasProperty Funktion nimmt implizit an, dass das Objekt eine Properties 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 namens Properties hat und es ist von DAO.Properties . Dies kann zur Kompilierzeit überprüft werden.
  • Statt nur einem Boolean Als Ergebnis können wir auch die abgerufene Property erhalten Objekt, sondern nur auf den Erfolg. Wenn wir scheitern, wird die OutProperty Parameter ist Nothing . Wir werden weiterhin den Boolean 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 true zurückgibt , das 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 wir Err.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 einen false 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.