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.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow