Ricerca…


Astrazione

I livelli di astrazione aiutano a determinare quando suddividere le cose.

L'astrazione si ottiene implementando funzionalità con codice sempre più dettagliato. Il punto di ingresso di una macro dovrebbe essere una piccola procedura con un alto livello di astrazione che renda facile capire a colpo d'occhio cosa sta succedendo:

Public Sub DoSomething()
    With New SomeForm
        Set .Model = CreateViewModel
        .Show vbModal            
        If .IsCancelled Then Exit Sub
        ProcessUserData .Model
    End With
End Sub

La procedura DoSomething ha un alto livello di astrazione : possiamo dire che sta visualizzando un modulo e creando un modello e passando quell'oggetto ad una procedura ProcessUserData che sa cosa fare con esso - il modo in cui il modello viene creato è il lavoro di un'altra procedura:

Private Function CreateViewModel() As ISomeModel
    Dim result As ISomeModel
    Set result = SomeModel.Create(Now, Environ$("UserName"))
    result.AvailableItems = GetAvailableItems
    Set CreateViewModel = result
End Function

La funzione CreateViewModel è responsabile solo della creazione di alcune istanze di ISomeModel . Parte di questa responsabilità consiste nell'acquisire una serie di elementi disponibili : il modo in cui questi elementi vengono acquisiti è un dettaglio di implementazione che è astratto rispetto alla procedura GetAvailableItems :

Private Function GetAvailableItems() As Variant
    GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function

Qui la procedura sta leggendo i valori disponibili da un intervallo denominato su un foglio di lavoro DataSheet . Potrebbe benissimo leggerli da un database, oppure i valori potrebbero essere hardcoded: è un dettaglio di implementazione che non preoccupa nessuno dei più alti livelli di astrazione.

incapsulamento

L'incapsulamento nasconde i dettagli di implementazione dal codice client.

L'esempio Handling QueryClose dimostra l'incapsulamento: il modulo ha un controllo casella di controllo, ma il suo codice client non funziona direttamente con esso: la casella di spunta è un dettaglio dell'implementazione , ciò che il codice client deve sapere è se l'impostazione è abilitata o meno.

Quando il valore della casella di controllo cambia, il gestore assegna un membro del campo privato:

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

E quando il codice client vuole leggere quel valore, non ha bisogno di preoccuparsi di una checkbox - invece usa semplicemente la proprietà SomeOtherSetting :

Public Property Get SomeOtherSetting() As Boolean
    SomeOtherSetting = this.SomeOtherSetting
End Property

La proprietà SomeOtherSetting incapsula lo stato della casella di controllo; il codice cliente non ha bisogno di sapere che c'è una casella di controllo, solo che c'è un'impostazione con un valore booleano. Incapsulando il valore Boolean , abbiamo aggiunto un livello di astrazione attorno alla casella di controllo.


Utilizzare le interfacce per rafforzare l'immutabilità

Facciamo un ulteriore passo avanti incapsulando il modello del modulo in un modulo di classe dedicato. Ma se abbiamo creato una Public Property per UserName e Timestamp , dovremmo esporre Property Let accessors, rendendo le proprietà mutabili, e non vogliamo che il codice client abbia la possibilità di modificare questi valori dopo che sono stati impostati.

La funzione CreateViewModel nell'esempio Abstraction restituisce una classe ISomeModel : questa è la nostra interfaccia e assomiglia a qualcosa del genere:

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

Nota Le proprietà Timestamp e UserName espongono solo una Property Get accesso. Ora la classe SomeModel può implementare quell'interfaccia:

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

I membri dell'interfaccia sono tutti Private e tutti i membri dell'interfaccia devono essere implementati affinché il codice possa essere compilato. I membri Public non fanno parte dell'interfaccia e pertanto non sono esposti al codice scritto contro l'interfaccia ISomeModel .


Utilizzo di un metodo di fabbrica per simulare un costruttore

Utilizzando un attributo VB_PredeclaredId , possiamo rendere la classe SomeModel un'istanza predefinita e scrivere una funzione che funzioni come un membro a livello di testo ( Shared in VB.NET, static in C #) che il codice client può chiamare senza dover prima creare un esempio, come abbiamo fatto qui:

Private Function CreateViewModel() As ISomeModel
    Dim result As ISomeModel
    Set result = SomeModel.Create(Now, Environ$("UserName"))
    result.AvailableItems = GetAvailableItems
    Set CreateViewModel = result
End Function

Questo metodo factory assegna i valori di proprietà che sono di sola lettura quando si accede dall'interfaccia ISomeModel , qui Timestamp e 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

E ora possiamo codificare l'interfaccia ISomeModel , che espone Timestamp e UserName come proprietà di sola lettura che non possono mai essere riassegnate (purché il codice sia scritto sull'interfaccia).

Polimorfismo

Il polimorfismo è la capacità di presentare la stessa interfaccia per diverse implementazioni sottostanti.

La possibilità di implementare interfacce consente di disaccoppiare completamente la logica dell'applicazione dall'interfaccia utente o dal database o da questo o quel foglio di lavoro.

Supponiamo tu abbia un'interfaccia ISomeView che il modulo stesso implementa:

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

Il code-behind del modulo potrebbe assomigliare a questo:

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

Ma poi, nulla vieta di creare un altro modulo di classe che implementa l'interfaccia di ISomeView senza essere un modulo utente - questa potrebbe essere una classe SomeViewMock :

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

E ora possiamo cambiare il codice che funziona con un UserForm e farlo funzionare sull'interfaccia di ISomeView , ad esempio assegnandogli il modulo come parametro invece di istanziarlo:

Public Sub DoSomething(ByVal view As ISomeView)
    With view
        Set .Model = CreateViewModel
        .Show
        If .IsCancelled Then Exit Sub
        ProcessUserData .Model
    End With
End Sub

Poiché il metodo DoSomething dipende da un'interfaccia (ad esempio un'astrazione ) e non da una classe concreta (ad es. Un UserForm specifico), possiamo scrivere un test unitario che garantisce che ProcessUserData non venga eseguito quando view.IsCancelled è True , rendendo il nostro test crea un'istanza SomeViewMock , impostando la proprietà IsCancelled su True e passando a DoSomething .


Il codice verificabile dipende dalle astrazioni

È possibile eseguire test di unità in VBA, ci sono componenti aggiuntivi che possono essere integrati nell'IDE. Ma quando il codice è strettamente associato a un foglio di lavoro, a un database, a un modulo o al file system, il test dell'unità inizia a richiedere un foglio di lavoro, un database, un modulo o un file system effettivo e queste dipendenze sono un nuovo errore fuori controllo indica che il codice verificabile deve essere isolato, in modo che i test unitari non richiedano un foglio di lavoro, un database, un modulo o un file system effettivi.

Scrivendo il codice contro le interfacce, in un modo che consente al codice di test di iniettare implementazioni di stub / mock (come nell'esempio SomeViewMock sopra), puoi scrivere test in un "ambiente controllato" e simulare cosa succede quando ognuno dei 42 possibili permutazioni delle interazioni dell'utente sui dati del modulo, anche senza visualizzare una volta un modulo e facendo clic manualmente su un controllo modulo.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow