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.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow