VBA
Objektorienterad VBA
Sök…
Abstraktion
Abstraktionsnivåer hjälper till att bestämma när man ska dela upp saker.
Abstraktion uppnås genom att implementera funktionalitet med allt mer detaljerad kod. En makros inträde bör vara en liten procedur med en hög abstraktionsnivå som gör det enkelt att snabbt förstå vad som händer:
Public Sub DoSomething()
With New SomeForm
Set .Model = CreateViewModel
.Show vbModal
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
DoSomething
proceduren har en hög abstraktionsnivå : vi kan säga att den visar ett formulär och skapar en modell, och överför det objektet till något ProcessUserData
förfarande som vet vad man ska göra med det - hur modellen skapas är jobbet med en annan procedur:
Private Function CreateViewModel() As ISomeModel
Dim result As ISomeModel
Set result = SomeModel.Create(Now, Environ$("UserName"))
result.AvailableItems = GetAvailableItems
Set CreateViewModel = result
End Function
CreateViewModel
funktionen ansvarar bara för att skapa någon ISomeModel
instans. En del av det ansvaret är att skaffa en mängd tillgängliga objekt - hur dessa objekt förvärvas är en implementeringsdetalj som abstraheras bakom proceduren GetAvailableItems
:
Private Function GetAvailableItems() As Variant
GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function
Här läser proceduren de tillgängliga värdena från ett namngivet intervall i ett DataSheet
kalkylblad. Det kan lika bra vara att läsa dem från en databas, eller värdena kan vara hårdkodade: det är en implementeringsdetalj som inte är oroande för någon av de högre abstraktionsnivåerna.
inkapsling
Inkapsling döljer implementeringsdetaljer från klientkoden.
Exemplet Hantering av QueryClose visar inkapsling: formuläret har en kryssrutekontroll, men dess klientkod fungerar inte direkt med den - kryssrutan är en implementeringsdetalj , vad klientkoden behöver veta är om inställningen är aktiverad eller inte.
När kryssrutans värde ändras tilldelar hanteraren en privat fältmedlem:
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
Och när klientkoden vill läsa det värdet behöver den inte oroa sig för en kryssruta - istället använder den SomeOtherSetting
egenskapen SomeOtherSetting
:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
SomeOtherSetting
inkapslar kryssrutans tillstånd; klientkod behöver inte veta att det finns en kryssruta inblandad, bara att det finns en inställning med ett booleskt värde. Genom att inkapsla det Boolean
värdet har vi lagt till ett abstraktionslager runt kryssrutan.
Använda gränssnitt för att upprätthålla immutability
Låt oss driva det ytterligare ett steg genom att kapsla in formens modell i en dedikerad klassmodul. Men om vi skapade en Public Property
för UserName
och Timestamp
, skulle vi behöva avslöja Property Let
till Property Let
, vilket gör egenskaperna muterbara och vi vill inte att klientkoden ska kunna ändra dessa värden efter att de är inställda.
Funktionen CreateViewModel
i Abstraktionsexemplet returnerar en ISomeModel
klass: det är vårt gränssnitt och det ser ut så här:
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
Observera Timestamp
och UserName
exponerar endast en Property Get
åtkomst. Nu SomeModel
klassen SomeModel
implementera det gränssnittet:
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
Gränssnittsmedlemmarna är alla Private
, och alla medlemmar i gränssnittet måste implementeras för att koden ska kunna sammanställas. Public
medlemmar ingår inte i gränssnittet och utsätts därför inte för kod skriven mot ISomeModel
gränssnittet.
Använda en fabriksmetod för att simulera en konstruktör
Med hjälp av ett VB_PredeclaredId- attribut kan vi göra att klassen SomeModel
har en standardinstans och skriva en funktion som fungerar som en typnivå ( Shared
i VB.NET, static
i C #) -medlem som klientkoden kan ringa utan att behöva först skapa en instans, som vi gjorde här:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
Denna fabriksmetod tilldelar egenskapens värden som är skrivskyddade när de nås från ISomeModel
gränssnittet, här Timestamp
och 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
Och nu kan vi koda mot ISomeModel
gränssnittet, som exponerar Timestamp
och UserName
som skrivskyddsegenskaper som aldrig kan tilldelas igen (så länge koden skrivs mot gränssnittet).
polymorfism
Polymorfism är förmågan att presentera samma gränssnitt för olika underliggande implementationer.
Möjligheten att implementera gränssnitt gör det möjligt att helt koppla bort applikationslogiken från UI, eller från databasen, eller från det här eller det kalkylbladet.
Säg att du har ett ISomeView
gränssnitt som själva formuläret implementerar:
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
Formulärets kod bakom kan se ut så här:
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
Men då förbjuder ingenting att skapa en annan klassmodul som implementerar ISomeView
gränssnittet utan att vara en användarform - det kan vara en SomeViewMock
klass:
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
Och nu kan vi ändra koden som fungerar med en UserForm
och få den att fungera från ISomeView
gränssnittet, t.ex. genom att ge det formen som en parameter istället för att instansera det:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Eftersom DoSomething
metoden beror på ett gränssnitt (dvs. en abstraktion ) och inte en konkret klass (t.ex. en specifik UserForm
), kan vi skriva ett automatiserat enhetstest som säkerställer att ProcessUserData
inte körs när view.IsCancelled
är True
, genom att göra vårt test skapa en SomeViewMock
instans, ställa IsCancelled
egenskapen IsCancelled
till True
och skicka den till DoSomething
.
Testbar kod beror på abstraktioner
Att skriva enhetstester i VBA kan göras, det finns tillägg där ute som till och med integrerar det i IDE. Men när kod är tätt kopplad till ett kalkylblad, en databas, ett formulär eller filsystemet, börjar enhetstestet som kräver ett faktiskt kalkylblad, databas, formulär eller filsystem - och dessa beroenden är ett nytt fel utan kontroll poäng som testbar kod bör isolera, så att enhetstester inte kräver ett faktiskt kalkylblad, databas, form eller filsystem.
Genom att skriva kod mot gränssnitt, på ett sätt som tillåter testkod att injicera stub / mock-implementationer (som ovanstående SomeViewMock
exempel), kan du skriva tester i en "kontrollerad miljö" och simulera vad som händer när varje enskild av de 42 möjliga permutationer av användarinteraktioner på formulärets data, utan en gång att visa ett formulär och manuellt klicka på en blankettkontroll.