VBA
Objektorientierte VBA
Suche…
Abstraktion
Abstraktionsebenen bestimmen, wann die Dinge aufgeteilt werden müssen.
Die Abstraktion wird durch die Implementierung von Funktionen mit immer detaillierterem Code erreicht. Der Einstiegspunkt eines Makros sollte eine kleine Prozedur mit einem hohen Abstraktionsgrad sein , die es ermöglicht, auf einen Blick zu erfassen, was los ist:
Public Sub DoSomething()
With New SomeForm
Set .Model = CreateViewModel
.Show vbModal
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Die DoSomething
Prozedur hat einen hohen Abstraktionsgrad : Wir können feststellen, dass sie ein Formular anzeigt und ein Modell erstellt und dieses Objekt an eine ProcessUserData
Prozedur ProcessUserData
, die weiß, was mit ihr zu tun ist - wie das Modell erstellt wird, ist Aufgabe einer anderen Prozedur:
Private Function CreateViewModel() As ISomeModel
Dim result As ISomeModel
Set result = SomeModel.Create(Now, Environ$("UserName"))
result.AvailableItems = GetAvailableItems
Set CreateViewModel = result
End Function
Die Funktion CreateViewModel
ist nur für das Erstellen einer ISomeModel
Instanz verantwortlich. Zu dieser Verantwortung gehört auch der Erwerb einer Reihe verfügbarer Elemente. Wie diese Elemente erworben werden, ist ein Implementierungsdetail, das hinter der GetAvailableItems
Prozedur GetAvailableItems
wird:
Private Function GetAvailableItems() As Variant
GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function
Hier liest die Prozedur die verfügbaren Werte aus einem benannten Bereich in einem DataSheet
Arbeitsblatt. Es könnte genauso gut aus einer Datenbank gelesen werden, oder die Werte könnten hart codiert sein: Dies ist ein Implementierungsdetail , das für keine der höheren Abstraktionsstufen von Belang ist.
Verkapselung
Encapsulation verbirgt Implementierungsdetails vor Clientcode.
Das Handling QueryClose- Beispiel veranschaulicht die Kapselung: Das Formular verfügt über ein Kontrollkästchen, der Clientcode funktioniert jedoch nicht direkt damit. Das Kontrollkästchen enthält Implementierungsdetails. Der Clientcode muss wissen, ob die Einstellung aktiviert ist oder nicht.
Wenn sich der Kontrollkästchenwert ändert, weist der Handler ein privates Feldmitglied zu:
Private Type TView IsCancelled As Boolean SomeOtherSetting As Boolean 'other properties skipped for brievety End Type Private this As TView '... Private Sub SomeOtherSettingInput_Change() this.SomeOtherSetting = CBool(SomeOtherSettingInput.Value) End Sub
Wenn der Clientcode diesen Wert lesen möchte, muss er sich nicht um ein Kontrollkästchen kümmern, sondern verwendet stattdessen die SomeOtherSetting
Eigenschaft:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
Die SomeOtherSetting
Eigenschaft kapselt den SomeOtherSetting
des Kontrollkästchens. Client-Code muss nicht wissen, dass ein Kontrollkästchen vorhanden ist, nur dass es eine Einstellung mit einem booleschen Wert gibt. Durch das Einkapseln des Boolean
Werts haben wir dem Kontrollkästchen eine Abstraktionsebene hinzugefügt.
Verwendung von Schnittstellen zur Durchsetzung der Unveränderlichkeit
Lassen Sie uns das noch einen Schritt weiter gehen, indem Sie das Modell des Formulars in einem dedizierten Klassenmodul kapseln . Wenn wir jedoch eine Public Property
für den UserName
und den Timestamp
UserName
, müssten wir die Property Let
UserName
, wodurch die Eigenschaften veränderbar werden. Wir möchten nicht, dass der Clientcode diese Werte nach dem UserName
ändern kann.
Die CreateViewModel
Funktion im Abstraction- Beispiel gibt eine ISomeModel
Klasse zurück: ISomeModel
ist unsere Schnittstelle und sieht etwa so aus:
Option Explicit
Public Property Get Timestamp() As Date
End Property
Public Property Get UserName() As String
End Property
Public Property Get AvailableItems() As Variant
End Property
Public Property Let AvailableItems(ByRef value As Variant)
End Property
Public Property Get SomeSetting() As String
End Property
Public Property Let SomeSetting(ByVal value As String)
End Property
Public Property Get SomeOtherSetting() As Boolean
End Property
Public Property Let SomeOtherSetting(ByVal value As Boolean)
End Property
Hinweis Timestamp
und UserName
Eigenschaften aussetzen nur ein Property Get
Accessor. Nun kann die SomeModel
Klasse diese Schnittstelle implementieren:
Option Explicit
Implements ISomeModel
Private Type TModel
Timestamp As Date
UserName As String
SomeSetting As String
SomeOtherSetting As Boolean
AvailableItems As Variant
End Type
Private this As TModel
Private Property Get ISomeModel_Timestamp() As Date
ISomeModel_Timestamp = this.Timestamp
End Property
Private Property Get ISomeModel_UserName() As String
ISomeModel_UserName = this.UserName
End Property
Private Property Get ISomeModel_AvailableItems() As Variant
ISomeModel_AvailableItems = this.AvailableItems
End Property
Private Property Let ISomeModel_AvailableItems(ByRef value As Variant)
this.AvailableItems = value
End Property
Private Property Get ISomeModel_SomeSetting() As String
ISomeModel_SomeSetting = this.SomeSetting
End Property
Private Property Let ISomeModel_SomeSetting(ByVal value As String)
this.SomeSetting = value
End Property
Private Property Get ISomeModel_SomeOtherSetting() As Boolean
ISomeModel_SomeOtherSetting = this.SomeOtherSetting
End Property
Private Property Let ISomeModel_SomeOtherSetting(ByVal value As Boolean)
this.SomeOtherSetting = value
End Property
Public Property Get Timestamp() As Date
Timestamp = this.Timestamp
End Property
Public Property Let Timestamp(ByVal value As Date)
this.Timestamp = value
End Property
Public Property Get UserName() As String
UserName = this.UserName
End Property
Public Property Let UserName(ByVal value As String)
this.UserName = value
End Property
Public Property Get AvailableItems() As Variant
AvailableItems = this.AvailableItems
End Property
Public Property Let AvailableItems(ByRef value As Variant)
this.AvailableItems = value
End Property
Public Property Get SomeSetting() As String
SomeSetting = this.SomeSetting
End Property
Public Property Let SomeSetting(ByVal value As String)
this.SomeSetting = value
End Property
Public Property Get SomeOtherSetting() As Boolean
SomeOtherSetting = this.SomeOtherSetting
End Property
Public Property Let SomeOtherSetting(ByVal value As Boolean)
this.SomeOtherSetting = value
End Property
Die Schnittstellenmitglieder sind alle Private
, und alle Mitglieder der Schnittstelle müssen implementiert werden, damit der Code kompiliert werden kann. Die Public
Member sind nicht Teil der Schnittstelle und unterliegen daher nicht dem für die ISomeModel
Schnittstelle geschriebenen Code.
Verwenden einer Factory-Methode zum Simulieren eines Konstruktors
Mit einem VB_PredeclaredId- Attribut können wir der SomeModel
Klasse eine Standardinstanz SomeModel
und eine Funktion schreiben, die wie ein SomeModel
der SomeModel
( Shared
in VB.NET, static
in C #) funktioniert, das der Clientcode aufrufen kann, ohne dass er zuerst erstellt werden muss ein Beispiel, wie wir es hier gemacht haben:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
Diese Factory-Methode weist die Eigenschaftswerte zu, die schreibgeschützt sind, wenn von der ISomeModel
Schnittstelle aus ISomeModel
wird, hier Timestamp
und UserName
:
Public Function Create(ByVal pTimeStamp As Date, ByVal pUserName As String) As ISomeModel
With New SomeModel
.Timestamp = pTimeStamp
.UserName = pUserName
Set Create = .Self
End With
End Function
Public Property Get Self() As ISomeModel
Set Self = Me
End Property
Und jetzt können wir gegen die ISomeModel
Schnittstelle ISomeModel
, die Timestamp
und UserName
als schreibgeschützte Eigenschaften UserName
, die niemals neu zugewiesen werden können (solange der Code gegen die Schnittstelle geschrieben wird).
Polymorphismus
Polymorphismus ist die Fähigkeit, dieselbe Schnittstelle für verschiedene zugrunde liegende Implementierungen bereitzustellen.
Durch die Möglichkeit, Schnittstellen zu implementieren, kann die Anwendungslogik vollständig von der Benutzeroberfläche oder von der Datenbank oder von diesem oder diesem Arbeitsblatt entkoppelt werden.
ISomeView
, Sie haben eine ISomeView
Schnittstelle, die das Formular selbst implementiert:
Option Explicit
Public Property Get IsCancelled() As Boolean
End Property
Public Property Get Model() As ISomeModel
End Property
Public Property Set Model(ByVal value As ISomeModel)
End Property
Public Sub Show()
End Sub
Der Code-Behind des Formulars könnte folgendermaßen aussehen:
Option Explicit
Implements ISomeView
Private Type TView
IsCancelled As Boolean
Model As ISomeModel
End Type
Private this As TView
Private Property Get ISomeView_IsCancelled() As Boolean
ISomeView_IsCancelled = this.IsCancelled
End Property
Private Property Get ISomeView_Model() As ISomeModel
Set ISomeView_Model = this.Model
End Property
Private Property Set ISomeView_Model(ByVal value As ISomeModel)
Set this.Model = value
End Property
Private Sub ISomeView_Show()
Me.Show vbModal
End Sub
Private Sub SomeOtherSettingInput_Change()
this.Model.SomeOtherSetting = CBool(SomeOtherSettingInput.Value)
End Sub
'...other event handlers...
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub CancelButton_Click()
this.IsCancelled = True
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
this.IsCancelled = True
Me.Hide
End If
End Sub
Allerdings verbietet nichts, ein anderes Klassenmodul zu erstellen, das die ISomeView
Schnittstelle implementiert, ohne ein Benutzerformular zu sein - dies könnte eine SomeViewMock
Klasse sein:
Option Explicit
Implements ISomeView
Private Type TView
IsCancelled As Boolean
Model As ISomeModel
End Type
Private this As TView
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Let IsCancelled(ByVal value As Boolean)
this.IsCancelled = value
End Property
Private Property Get ISomeView_IsCancelled() As Boolean
ISomeView_IsCancelled = this.IsCancelled
End Property
Private Property Get ISomeView_Model() As ISomeModel
Set ISomeView_Model = this.Model
End Property
Private Property Set ISomeView_Model(ByVal value As ISomeModel)
Set this.Model = value
End Property
Private Sub ISomeView_Show()
'do nothing
End Sub
Und jetzt können wir den Code ändern, der mit einer UserForm
und die ISomeView
Schnittstelle zum ISomeView
, z. B. indem Sie die Form als Parameter ISomeView
, anstatt sie zu instanziieren:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Da die DoSomething
Methode von einer Schnittstelle (z. B. einer Abstraktion ) und nicht von einer konkreten Klasse (z. B. einer bestimmten UserForm
) UserForm
, können wir einen automatisierten UserForm
schreiben, der gewährleistet, dass ProcessUserData
nicht ausgeführt wird, wenn view.IsCancelled
True
hat test Erstellen Sie eine SomeViewMock
Instanz, setzen Sie die IsCancelled
Eigenschaft auf True
und übergeben Sie sie an DoSomething
.
Testbarer Code hängt von Abstraktionen ab
Unit-Tests können in VBA geschrieben werden, es gibt Add-Ins, die es sogar in die IDE integrieren. Wenn Code jedoch eng mit einem Arbeitsblatt, einer Datenbank, einem Formular oder dem Dateisystem gekoppelt ist, erfordert der Komponententest ein tatsächliches Arbeitsblatt, eine Datenbank, ein Formular oder ein Dateisystem. Diese Abhängigkeiten sind ein neuer Out-of-Control-Fehler Punkte, die der zu testende Code isolieren sollte, sodass für Einzeltests kein Arbeitsblatt, keine Datenbank, kein Formular oder kein Dateisystem erforderlich ist.
Durch das Schreiben von Code gegen Schnittstellen, sodass SomeViewMock
Stub / Mock-Implementierungen (wie das obige SomeViewMock
Beispiel) injizieren kann, können Sie Tests in einer "kontrollierten Umgebung" schreiben und simulieren, was passiert, wenn jede einzelne der 42 möglich ist Permutationen von Benutzerinteraktionen auf die Daten des Formulars, ohne auch nur ein Formular anzuzeigen und manuell auf ein Formularsteuerelement zu klicken.