VBA
VBA orientato agli oggetti
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.