VBA
Objectgeoriënteerde VBA
Zoeken…
Abstractie
Abstractieniveaus helpen bepalen wanneer dingen moeten worden opgesplitst.
Abstractie wordt bereikt door functionaliteit met steeds gedetailleerdere code te implementeren. Het beginpunt van een macro moet een kleine procedure zijn met een hoog abstractieniveau waarmee u in één oogopslag kunt zien wat er aan de hand is:
Public Sub DoSomething()
With New SomeForm
Set .Model = CreateViewModel
.Show vbModal
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
De DoSomething
procedure heeft een hoog abstractieniveau : we kunnen zien dat het een formulier ProcessUserData
en een model maakt en dat object ProcessUserData
aan een ProcessUserData
procedure die weet wat ermee te doen - hoe het model wordt gemaakt, is de taak van een andere procedure:
Private Function CreateViewModel() As ISomeModel
Dim result As ISomeModel
Set result = SomeModel.Create(Now, Environ$("UserName"))
result.AvailableItems = GetAvailableItems
Set CreateViewModel = result
End Function
De functie CreateViewModel
is alleen verantwoordelijk voor het maken van een ISomeModel
instantie. Een deel van die verantwoordelijkheid is het verkrijgen van een reeks beschikbare items - hoe deze items worden verkregen is een implementatiedetail dat is geabstraheerd achter de procedure GetAvailableItems
:
Private Function GetAvailableItems() As Variant
GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function
Hier leest de procedure de beschikbare waarden uit een benoemd bereik op een DataSheet
werkblad. Het zou net zo goed kunnen zijn om ze uit een database te lezen, of de waarden kunnen hard gecodeerd zijn: het is een implementatiedetail dat geen enkele zorg is voor een van de hogere abstractieniveaus.
inkapseling
Inkapseling verbergt implementatiedetails van klantcode.
Het Handling QueryClose- voorbeeld laat inkapseling zien: het formulier heeft een selectievakje, maar de clientcode werkt er niet rechtstreeks mee - het selectievakje is een implementatiedetail , wat de clientcode moet weten is of de instelling is ingeschakeld of niet.
Wanneer de waarde van het selectievakje verandert, wijst de handler een lid van het privéveld toe:
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
En wanneer de SomeOtherSetting
die waarde wil lezen, hoeft hij zich geen zorgen te maken over een selectievakje - in plaats daarvan gebruikt hij gewoon de eigenschap SomeOtherSetting
:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
De eigenschap SomeOtherSetting
bevat de status van het selectievakje; clientcode hoeft niet te weten dat er een selectievakje bij betrokken is, alleen dat er een instelling met een Booleaanse waarde is. Door de Boolean
waarde in te kapselen , hebben we een abstractielaag rondom het selectievakje toegevoegd.
Het gebruik van interfaces om onveranderlijkheid af te dwingen
Laten we dat een stap verder brengen door het model van het formulier in een speciale klassemodule in te kapselen . Maar als we een verzonnen Public Property
voor de UserName
en Timestamp
, zouden we moeten blootstellen Property Let
accessors, waardoor de eigenschappen veranderlijk, en we willen niet dat de client-code om de mogelijkheid om nadat ze zijn set deze waarden te wijzigen.
De functie CreateViewModel
in het voorbeeld Abstraction retourneert een klasse ISomeModel
: dat is onze interface en deze ziet er ongeveer zo uit:
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
Let op de eigenschappen Timestamp
en UserName
alleen een Property Get
Accessor-gebruiker weer. Nu kan de SomeModel
klasse die interface implementeren:
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
De interface-leden zijn allemaal Private
en alle leden van de interface moeten worden geïmplementeerd om de code te compileren. De Public
leden maken geen deel uit van de interface en zijn daarom niet blootgesteld aan code die tegen de ISomeModel
interface is geschreven.
Een fabrieksmethode gebruiken om een constructor te simuleren
Met behulp van een kenmerk VB_PredeclaredId kunnen we ervoor zorgen dat de klasse SomeModel
een standaardinstantie heeft en een functie kan schrijven die werkt als een lid op type niveau ( Shared
in VB.NET, static
in C #) dat de clientcode kan oproepen zonder eerst te hoeven maken een voorbeeld, zoals we hier deden:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
Deze fabrieksmethode wijst de eigenschapswaarden toe die alleen-lezen zijn wanneer ze worden geopend vanuit de ISomeModel
interface, hier Timestamp
en 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
En nu kunnen we coderen tegen de ISomeModel
interface, waardoor Timestamp
en UserName
als alleen-lezen eigenschappen die nooit opnieuw kunnen worden toegewezen (zolang de code tegen de interface wordt geschreven).
polymorfisme
Polymorfisme is de mogelijkheid om dezelfde interface te presenteren voor verschillende onderliggende implementaties.
De mogelijkheid om interfaces te implementeren maakt het mogelijk om de applicatielogica volledig los te koppelen van de UI, of van de database, of van dit of dat werkblad.
Stel dat u een ISomeView
interface hebt die het formulier zelf implementeert:
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
De code-achter van het formulier kan er zo uitzien:
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
Maar dan verbiedt niets het creëren van een andere ISomeView
die de ISomeView
interface implementeert zonder een gebruikersformulier te zijn - dit zou een SomeViewMock
klasse kunnen zijn:
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
En nu kunnen we de code die werkt met een UserForm
en laten werken vanuit de ISomeView
interface, bijvoorbeeld door het de vorm als parameter te geven in plaats van het te instantiëren:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Omdat de DoSomething
methode afhankelijk is van een interface (bijv. Een abstractie ) en niet van een concrete klasse (bijv. Een specifieke UserForm
), kunnen we een geautomatiseerde eenheidstest schrijven die ervoor zorgt dat ProcessUserData
niet wordt uitgevoerd wanneer view.IsCancelled
True
, door onze test maak een SomeViewMock
instantie, IsCancelled
eigenschap IsCancelled
in op True
en DoSomething
deze door aan DoSomething
.
Testbare code is afhankelijk van abstracties
Schrijven van unit-testen in VBA kan worden gedaan, er zijn add-ins die het zelfs in de IDE integreren. Maar wanneer code nauw is gekoppeld aan een werkblad, een database, een formulier of het bestandssysteem, begint de eenheidstest een echt werkblad, database, formulier of bestandssysteem te vereisen - en deze afhankelijkheden zijn nieuwe onbeheersbare mislukkingen wijst erop dat testbare code moet isoleren, zodat voor unit-tests geen echt werkblad, database, formulier of bestandssysteem nodig is.
Door code tegen interfaces te schrijven, op een manier waarmee testcode stub / mock-implementaties kan injecteren (zoals het bovenstaande SomeViewMock
voorbeeld), kunt u tests schrijven in een "gecontroleerde omgeving" en simuleren wat er gebeurt wanneer elk van de 42 mogelijke permutaties van gebruikersinteracties op de gegevens van het formulier, zonder zelfs maar één keer een formulier weer te geven en handmatig op een formulierbesturing te klikken.