Recherche…
Abstraction
Les niveaux d'abstraction aident à déterminer quand diviser les choses.
L'abstraction est obtenue en implémentant des fonctionnalités avec un code de plus en plus détaillé. Le point d’entrée d’une macro doit être une petite procédure avec un niveau d’abstraction élevé qui facilite la compréhension en un coup d’œil de ce qui se passe:
Public Sub DoSomething()
With New SomeForm
Set .Model = CreateViewModel
.Show vbModal
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
La procédure DoSomething
a un niveau d' abstraction élevé : on peut dire qu'elle affiche un formulaire et crée un modèle, et en passant cet objet à une procédure ProcessUserData
qui sait quoi en faire - comment le modèle est créé est le travail d'une autre procédure:
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 fonction CreateViewModel
est uniquement responsable de la création d'une instance ISomeModel
. Une partie de cette responsabilité consiste à acquérir un tableau d' éléments disponibles - la manière dont ces éléments sont acquis est un détail d'implémentation qui est résumé derrière la procédure GetAvailableItems
:
Private Function GetAvailableItems() As Variant
GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function
Ici, la procédure lit les valeurs disponibles d'une plage nommée sur une feuille de calcul DataSheet
. Cela pourrait tout aussi bien être de les lire à partir d'une base de données ou les valeurs pourraient être codées en dur: c'est un détail d'implémentation qui ne concerne aucun des niveaux d'abstraction les plus élevés.
Encapsulation
L'encapsulation masque les détails d'implémentation du code client.
L'exemple Handling QueryClose illustre l'encapsulation: le formulaire a un contrôle de case à cocher, mais son code client ne fonctionne pas directement - la case à cocher est un détail d'implémentation. Le code client doit savoir si le paramètre est activé ou non.
Lorsque la valeur de la case à cocher change, le gestionnaire affecte un membre de champ privé:
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
Et lorsque le code client veut lire cette valeur, il n’a pas à se soucier d’une case à cocher - au lieu de cela, il utilise simplement la propriété SomeOtherSetting
:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
La propriété SomeOtherSetting
encapsule l'état de la case à cocher; le code client n'a pas besoin de savoir qu'il y a une case à cocher, mais seulement qu'il y a un paramètre avec une valeur booléenne. En encapsulant la valeur Boolean
, nous avons ajouté une couche d'abstraction autour de la case à cocher.
Utiliser des interfaces pour renforcer l'immuabilité
Allons plus loin en encapsulant le modèle du formulaire dans un module de classe dédié. Mais si nous avons fait une Public Property
pour la UserName
et Timestamp
, nous devons exposer la Property Let
accesseurs, ce qui rend les propriétés mutable, et nous ne voulons pas le code client d'avoir la possibilité de changer ces valeurs après ils sont mis.
La fonction CreateViewModel
dans l'exemple Abstraction renvoie une classe ISomeModel
: c'est notre interface , et elle ressemble à ceci:
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
Notez que les propriétés Timestamp
et UserName
exposent uniquement un accesseur Property Get
. Maintenant, la classe SomeModel
peut implémenter cette interface:
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
Les membres de l'interface sont tous Private
, et tous les membres de l'interface doivent être implémentés pour que le code puisse être compilé. Les membres Public
ne font pas partie de l'interface et ne sont donc pas exposés au code écrit sur l'interface ISomeModel
.
Utilisation d'une méthode d'usine pour simuler un constructeur
En utilisant un attribut VB_PredeclaredId , nous pouvons faire en sorte que la classe SomeModel
ait une instance par défaut et écrire une fonction qui fonctionne comme un membre de type niveau ( Shared
en VB.NET, static
en C #) que le code client peut appeler sans avoir à créer au préalable une instance, comme nous l'avons fait ici:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
Cette méthode de fabrique attribue les valeurs de propriété en lecture seule lorsque vous y ISomeModel
depuis l'interface ISomeModel
, ici Timestamp
et 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
Et maintenant, nous pouvons coder sur l'interface ISomeModel
, qui expose Timestamp
et UserName
tant que propriétés en lecture seule qui ne peuvent jamais être réaffectées (tant que le code est écrit sur l'interface).
Polymorphisme
Le polymorphisme est la capacité de présenter la même interface pour différentes implémentations sous-jacentes.
La possibilité d'implémenter des interfaces permet de découpler complètement la logique d'application de l'interface utilisateur, de la base de données ou de telle ou telle feuille de travail.
Disons que vous avez une interface ISomeView
que le formulaire implémente lui-même:
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
Le code-behind du formulaire pourrait ressembler à ceci:
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
Mais alors, rien n'interdit de créer un autre module de classe qui implémente l'interface ISomeView
sans être un formulaire utilisateur - cela pourrait être une 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
Et maintenant, nous pouvons changer le code qui fonctionne avec un objet UserForm
et le faire fonctionner depuis l'interface ISomeView
, par exemple en lui donnant le formulaire en tant que paramètre au lieu de l'instancier:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Parce que la DoSomething
méthode dépend d'une interface ( par exemple une abstraction) et non pas une classe concrète (par exemple un spécifique UserForm
), nous pouvons écrire un test unitaire automatisé qui assure ProcessUserData
n'est pas exécutée lorsque view.IsCancelled
est True
, en faisant notre test créer une instance SomeViewMock
, en définissant sa propriété IsCancelled
sur True
, et en la transmettant à DoSomething
.
Le code testable dépend des abstractions
L'écriture de tests unitaires dans VBA peut être effectuée, il y a des compléments là-bas qui l'intègrent même dans l'EDI. Mais lorsque le code est étroitement couplé avec une feuille de calcul, une base de données, une forme, ou le système de fichiers, le test unitaire commence nécessitant une feuille de calcul réelle, base de données, sous forme ou système de fichiers - et ces dépendances sont nouvel échec hors de contrôle les points que le code testable doit isoler, de sorte que les tests unitaires ne nécessitent pas une feuille de calcul, une base de données, un formulaire ou un système de fichiers réel.
En écrivant du code sur des interfaces, de manière à ce que le code de test injecte des implémentations stub / mock (comme l'exemple SomeViewMock
ci-dessus), vous pouvez écrire des tests dans un "environnement contrôlé" et simuler ce qui se passe lorsque permutations des interactions de l'utilisateur sur les données du formulaire, sans même afficher un formulaire et cliquer manuellement sur un contrôle de formulaire.