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.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow