En conclusion du billet sur les classes, nous avions dit que le système de POO est moins poussé en VBA que dans d’autres langages.
Nous pouvons citer l’absence d’héritage notamment. L’héritage est un moyen de créer une classe spécialisée à partir d’une autre. Par exemple, nous pourrions imaginer une classe ClientParticulier
et une classe ClientEntreprise
héritant toutes deux d’une classe Client
afin d’étendre ou de modifier son comportement.
Néanmoins, il est tout de même possible de travailler avec des interfaces en VBA. Une interface est une sorte de contrat qui stipule ce que chaque classe implémentant cette interface doit définir. Cela est utile lorsque des objets de types différents doivent pouvoir être utilisés, en partie ou totalement, de manière similaire, par des propriétés ou des méthodes ayant un nom et un sens commun.
Par exemple, un canard, Superman et un avion sont tous les trois capables de voler, mais pas tout à fait pareil…. Nous pourrions imaginer une interface stipulant le nécessaire pour voler tel qu’une propriété altitude, une méthode décoller, une méthode atterrir, ….
Ce qu’il est important de noter ici, c’est que cette interface indique aux classes qu’elles doivent implémenter ces éléments, mais pas comment, laissant la liberté d’implémentation à celles-ci. Ainsi, en reprenant notre exemple, nous pourrions donc implémenter une méthode décoller avec un comportement différent (le canard ferait coin-coin, battrait des ailes et s’envolerait à petite vitesse tandis que l’avion allumerait les moteurs, accélérerait sur la piste puis s’envolerait à moyenne vitesse, et que Superman passerait en tenue adéquate de façon discrète avant de lever le poing et de s’envoler à grande vitesse en poussant sur ses jambes). Nous pourrions aussi choisir de rendre privée l’altitude de vol de Superman pour des raisons de confidentialité. Il faut juste qu’en tout temps le contrat soit respecté.
Au cours de ce billet, nous allons nous intéresser aux interfaces en VBA.
C’est parti !
Pour ce billet, nous nous placerons dans l’environnement Microsoft Office.
Définition
Comme indiqué en introduction, une interface est une sorte de contrat qui dit ce que doit définir les classes l’utilisant, mais pas comment.
Pour créer une interface, nous devons ajouter un module de classe. Pour son nom, il est d’usage en VBA de le préfixer par I.
Notre module de classe étant créé, nous pouvons ajouter des propriétés ou méthodes. Il suffit de les définir en Public
pour qu’elles fassent partie de l’interface :
Interface IInfoIdentite
Option Explicit
Public Property Let sNom(ByVal sNom As String) ' Propriété procédure property
End Property
Public Property Get sNom() As String ' Propriété procédure property
End Property
Public Property Set oAdresse(ByRef oAdresse As clsAdresse) ' Propriété procédure property
End Property
Les propriétés en tant que variables membres (mNom
par exemple) seront à définir dans les classes au moment de l’implémentation.
Pour que notre exemple compile, rajoutons une classe clsAdresse
.
Classe clsAdresse
Option Explicit
' Rien
Nous pouvons ajouter autant d’interfaces que nous le souhaitons. Une bonne pratique est d’avoir de petites interfaces spécifiques plutôt qu’une grosse interface générale. En effet, cela évite ainsi d’implémenter du code inutile, et il est plus simple de comprendre le champ d’actions des interfaces utilisées. C’est un des principes (le I) de l’approche SOLID.
Interface IInfoProfessionnelle
Option Explicit
Public sSiret As String ' Équivalent de Propriété procédure property Let + Get
Comme nous allons travailler avec des dictionnaires en liaison anticipée, nous ajoutons la référence Microsoft Scripting Runtime.
Interface ICommande
Option Explicit
Public Function NombreCommandes() As Integer ' Méthode
End Function
Public Sub AjouteCommande(ByRef dictCommande As Scripting.Dictionary) ' Méthode
End Sub
Et une dernière pour la route :
Interface IAffichable
Option Explicit
Public Function ToString() As String ' Méthode
End Function
Public Sub Affiche() ' Méthode
End Sub
Au cours de cette première section, nous avons vu comment définir des interfaces en VBA.
Utilisation
Pour utiliser une interface, on dit qu’on l’implémente d’où le mot-clef Implements
suivi du nom de l’interface.
Classe clsClient avant implémentation complète d’IInfoIdentite
Option Explicit
Implements IInfoIdentite
' ...
Notons que toutes les classes implémentent implicitement une interface : celle par défaut de la classe.
À ce stade, si nous compilons le projet, nous obtiendrons une erreur de compilation. C’est normal, car nous n’avons pas donné la définition du contenu de l’interface. Autrement dit, nous n’avons pas respecté le contrat ! Cela veut aussi dire que lorsque notre interface évoluera, il faudra faire le nécessaire pour continuer à respecter ce contrat.
Une petite astuce pour rajouter les éléments à implémenter est de passer par les zones de sélection en haut de l’éditeur de code.
Nous pouvons bien entendu écrire le code nous-mêmes, mais il faut faire attention à la syntaxe. Comme nous pouvons le voir dans l’image ci-dessus, le nom est construit sous la forme NomInterface_NomProprieteOuMethodeADefinir
.
Classe clsClient après implémentation complète d’IInfoIdentite
Option Explicit
Implements IInfoIdentite
Private mNom As String
Private mAdresse As clsAdresse
Public Property Get IInfoIdentite_sNom() As String
IInfoIdentite_sNom = mNom
End Property
Public Property Let IInfoIdentite_sNom(ByVal sNom As String)
mNom = sNom
End Property
Public Property Set IInfoIdentite_oAdresse(ByRef oAdresse As clsAdresse)
Set mAdresse = oAdresse
End Property
Nous pouvons continuer sur notre lancée et implémenter notre interface ICommande
. Pour cela, nous utiliserons également le code suivant placé dans un module standard :
Module Globals
Option Explicit
Option Private Module ' Pour empêcher la fonction d'être utilisable côté Excel
Public Enum ETypeCommande
eTypeCommandeClient
eTypeCommandeFournisseur
End Enum
Public Function GenereNumCommande(ByVal eTypeCommande As ETypeCommande) As String
' Static pour conserver les valeurs au fil des appels pour incrémenter
Static lCompteurCommandeClient As Long
Static lCompteurCommandeFournisseur As Long
Dim lCompteur As Long
Dim sDebut As String
Select Case eTypeCommande
Case eTypeCommandeClient
lCompteurCommandeClient = lCompteurCommandeClient + 1
lCompteur = lCompteurCommandeClient
sDebut = "CLIENT"
Case eTypeCommandeFournisseur
lCompteurCommandeFournisseur = lCompteurCommandeFournisseur + 1
lCompteur = lCompteurCommandeFournisseur
sDebut = "ACHAT"
End Select
GenereNumCommande = sDebut & Format(lCompteur, "00000000")
End Function
Classe clsClient après implémentation complète d’ICommande
Option Explicit
Implements IInfoIdentite
Implements ICommande
Private mNom As String
Private mAdresse As clsAdresse
Private mCommandes As Collection
Private Sub Class_Initialize()
Set mCommandes = New Collection
End Sub
Private Sub Class_Terminate()
' Les dictionnaires contenus ne sont pas supprimés explicitement ici
Set mCommandes = Nothing
End Sub
Public Property Get IInfoIdentite_sNom() As String
IInfoIdentite_sNom = mNom
End Property
Public Property Let IInfoIdentite_sNom(ByVal sNom As String)
mNom = sNom
End Property
Public Property Set IInfoIdentite_oAdresse(ByRef oAdresse As clsAdresse)
Set mAdresse = oAdresse
End Property
Private Function ICommande_NombreCommandes() As Integer
ICommande_NombreCommandes = mCommandes.Count
End Function
Public Sub ICommande_AjouteCommande(ByRef dictContenuCommande As Scripting.IDictionary)
Dim dictCommande As New Scripting.Dictionary
dictCommande.Add "Num", GenereNumCommande(eTypeCommandeClient)
dictCommande.Add "Contenu", dictContenuCommande
mCommandes.Add dictCommande
End Sub
Nous allons maintenant implémenter ces deux interfaces pour une classe fournisseur. Ce qu’il est intéressant de remarquer ici, c’est la divergence que nous pouvons faire par rapport à clsClient
. Nous pouvons donner une visibilité différente ou un comportement différent.
clsFournisseur
Option Explicit
Implements IInfoIdentite
Implements ICommande
Private mNom As String
Private mAdresse As clsAdresse
Private mQteMinimaleParArticle As Integer ' Spécifique
Private mCommandes As Collection
Private Sub Class_Initialize()
Set mCommandes = New Collection
End Sub
Private Sub Class_Terminate()
' Les dictionnaires contenus ne sont pas supprimés explicitement ici
Set mCommandes = Nothing
End Sub
Public Property Let QteMinimaleParArticle(ByVal iQteMinimaleParArticle As Integer)
' Spécifique
mQteMinimaleParArticle = iQteMinimaleParArticle
End Property
Private Property Get IInfoIdentite_sNom() As String
' Spécifique visibilité
IInfoIdentite_sNom = mNom
End Property
Public Property Let IInfoIdentite_sNom(ByVal sNom As String)
' Spécifique pour empêcher la modification du nom une fois défini
If mNom = "" Then
mNom = sNom
End If
End Property
Public Property Set IInfoIdentite_oAdresse(ByRef oAdresse As clsAdresse)
Set mAdresse = oAdresse
End Property
Private Function ICommande_NombreCommandes() As Integer
ICommande_NombreCommandes = mCommandes.Count
End Function
Public Sub ICommande_AjouteCommande(dictContenuCommande As Scripting.IDictionary)
Dim vPrix As Variant
' Spécifique où l'on s'assure que la quantité commandée au fournisseur est
' au moins égale à la quantité minimale lorsqu'une quantité minimale est paramétrée
Dim vReference As Variant
For Each vReference In dictContenuCommande.Keys
If dictContenuCommande(vReference) < mQteMinimaleParArticle And mQteMinimaleParArticle > 0 Then
dictContenuCommande(vReference) = mQteMinimaleParArticle
End If
Next vReference
Dim dictCommande As New Scripting.Dictionary
dictCommande.Add "Num", GenereNumCommande(eTypeCommandeFournisseur)
dictCommande.Add "Contenu", dictContenuCommande
mCommandes.Add dictCommande
End Sub
Au fil de cette section, nous avons vu comment mettre en œuvre une interface au sein d’une classe.
Code final
Pour terminer, voici le code final du projet.
Nous implémentons l’interface IAffichage
(dans clsClient
et clsFournisseur
) ainsi que l’interface IInfoProfessionnelle
(seulement dans clsFournisseur
) et nous testons notre programme au sein d’une procédure Main
.
Module Globals
Option Explicit
Option Private Module ' Pour empêcher la fonction d'être utilisable côté Excel
Public Enum ETypeCommande
eTypeCommandeClient
eTypeCommandeFournisseur
End Enum
Public Function GenereNumCommande(ByVal eTypeCommande As ETypeCommande) As String
' Static pour conserver les valeurs au fil des appels pour incrémenter
Static lCompteurCommandeClient As Long
Static lCompteurCommandeFournisseur As Long
Dim lCompteur As Long
Dim sDebut As String
Select Case eTypeCommande
Case eTypeCommandeClient
lCompteurCommandeClient = lCompteurCommandeClient + 1
lCompteur = lCompteurCommandeClient
sDebut = "CLIENT"
Case eTypeCommandeFournisseur
lCompteurCommandeFournisseur = lCompteurCommandeFournisseur + 1
lCompteur = lCompteurCommandeFournisseur
sDebut = "ACHAT"
End Select
GenereNumCommande = sDebut & Format(lCompteur, "00000000")
End Function
Interface IAffichable
Option Explicit
Public Function ToString() As String ' Méthode
End Function
Public Sub Affiche() ' Méthode
End Sub
Interface ICommande
Option Explicit
Public Function NombreCommandes() As Integer ' Méthode
End Function
Public Sub AjouteCommande(ByRef dictCommande As Scripting.Dictionary) ' Méthode
End Sub
Interface IInfoIdentite
Option Explicit
Public Property Let sNom(ByVal sNom As String) ' Propriété procédure property
End Property
Public Property Get sNom() As String ' Propriété procédure property
End Property
Public Property Set oAdresse(ByRef oAdresse As clsAdresse) ' Propriété procédure property
End Property
Interface IInfoProfessionnelle
Option Explicit
Public sSiret As String ' Équivalent de Propriété procédure property Let + Get
Classe clsAdresse
Option Explicit
' Rien
Classe clsClient
Option Explicit
Implements IInfoIdentite
Implements ICommande
Implements IAffichable
Private mNom As String
Private mAdresse As clsAdresse
Private mCommandes As Collection
Private Sub Class_Initialize()
Set mCommandes = New Collection
End Sub
Private Sub Class_Terminate()
' Les dictionnaires contenus ne sont pas supprimés explicitement ici
Set mCommandes = Nothing
End Sub
Public Property Get IInfoIdentite_sNom() As String
IInfoIdentite_sNom = mNom
End Property
Public Property Let IInfoIdentite_sNom(ByVal sNom As String)
mNom = sNom
End Property
Public Property Set IInfoIdentite_oAdresse(ByRef oAdresse As clsAdresse)
Set mAdresse = oAdresse
End Property
Private Function ICommande_NombreCommandes() As Integer
ICommande_NombreCommandes = mCommandes.Count
End Function
Public Sub ICommande_AjouteCommande(ByRef dictContenuCommande As Scripting.IDictionary)
Dim dictCommande As New Scripting.Dictionary
dictCommande.Add "Num", GenereNumCommande(eTypeCommandeClient)
dictCommande.Add "Contenu", dictContenuCommande
mCommandes.Add dictCommande
End Sub
Private Function IAffichable_ToString() As String
Dim sResult As String
sResult = "_________ Client : " & mNom & " _________" & vbNewLine
sResult = sResult & "Adresse :" & vbNewLine
If ICommande_NombreCommandes Then
sResult = sResult & vbNewLine
sResult = sResult & "Commandes :" & vbNewLine
Dim vCommande As Variant
Dim vReference As Variant
For Each vCommande In mCommandes
sResult = sResult & " " & vCommande("Num") & vbNewLine
For Each vReference In vCommande("Contenu")
sResult = sResult & " " & vCommande("Contenu")(vReference) & " X " & vReference & vbNewLine
Next vReference
Next vCommande
Else
sResult = sResult & "Aucune commande enregistrée" & vbNewLine
End If
sResult = sResult & "________________________" & vbNewLine
IAffichable_ToString = sResult
End Function
Public Sub IAffichable_Affiche()
Debug.Print IAffichable_ToString()
End Sub
Classe clsFournisseur
Option Explicit
Implements IInfoIdentite
Implements IInfoProfessionnelle
Implements ICommande
Implements IAffichable
Private mNom As String
Private mAdresse As clsAdresse
Private mSiret As String 'Spécifique
Private mQteMinimaleParArticle As Integer ' Spécifique
Private mCommandes As Collection
Private Sub Class_Initialize()
Set mCommandes = New Collection
End Sub
Private Sub Class_Terminate()
' Les dictionnaires contenus ne sont pas supprimés explicitement ici
Set mCommandes = Nothing
End Sub
Public Property Let QteMinimaleParArticle(ByVal iQteMinimaleParArticle As Integer)
' Spécifique
mQteMinimaleParArticle = iQteMinimaleParArticle
End Property
Private Property Get IInfoIdentite_sNom() As String
' Spécifique visibilité
IInfoIdentite_sNom = mNom
End Property
Public Property Let IInfoIdentite_sNom(ByVal sNom As String)
' Spécifique pour empêcher la modification du nom une fois défini
If mNom = "" Then
mNom = sNom
End If
End Property
Public Property Set IInfoIdentite_oAdresse(ByRef oAdresse As clsAdresse)
Set mAdresse = oAdresse
End Property
Public Property Let IInfoProfessionnelle_sSiret(ByVal sSiret As String)
mSiret = sSiret
End Property
Public Property Get IInfoProfessionnelle_sSiret() As String
IInfoProfessionnelle_sSiret = mSiret
End Property
Private Function ICommande_NombreCommandes() As Integer
ICommande_NombreCommandes = mCommandes.Count
End Function
Public Sub ICommande_AjouteCommande(dictContenuCommande As Scripting.IDictionary)
Dim vPrix As Variant
' Spécifique où l'on s'assure que la quantité commandée au fournisseur est la
' au moins égale à la quantité minimale lorsqu'une quantité minimale est paramétrée
Dim vReference As Variant
For Each vReference In dictContenuCommande.Keys
If dictContenuCommande(vReference) < mQteMinimaleParArticle And mQteMinimaleParArticle > 0 Then
dictContenuCommande(vReference) = mQteMinimaleParArticle
End If
Next vReference
Dim dictCommande As New Scripting.Dictionary
dictCommande.Add "Num", GenereNumCommande(eTypeCommandeFournisseur)
dictCommande.Add "Contenu", dictContenuCommande
mCommandes.Add dictCommande
End Sub
Private Function IAffichable_ToString() As String
Dim sResult As String
sResult = "_________ Fournisseur : " & mNom & " _________" & vbNewLine
sResult = sResult & "Adresse :" & vbNewLine
sResult = sResult & "N° siret : " & mSiret & vbNewLine
sResult = sResult & "Qté minimale / article : " & mQteMinimaleParArticle & vbNewLine
If ICommande_NombreCommandes Then
sResult = sResult & vbNewLine
sResult = sResult & "Commandes :" & vbNewLine
Dim vCommande As Variant
Dim vReference As Variant
For Each vCommande In mCommandes
sResult = sResult & " " & vCommande("Num") & vbNewLine
For Each vReference In vCommande("Contenu")
sResult = sResult & " " & vCommande("Contenu")(vReference) & " X " & vReference & vbNewLine
Next vReference
Next vCommande
Else
sResult = sResult & "Aucune commande enregistrée" & vbNewLine
End If
sResult = sResult & "________________________" & vbNewLine
IAffichable_ToString = sResult
End Function
Public Sub IAffichable_Affiche()
Debug.Print IAffichable_ToString()
End Sub
Module Main
Option Explicit
Private Sub Main()
Dim cEntites As New Collection
' Ajout de clients / fournisseurs
Dim oClient1 As New clsClient
oClient1.IInfoIdentite_sNom = "Dupont"
Set oClient1.IInfoIdentite_oAdresse = New clsAdresse
' Debug.Print (oClient1.IInfoIdentite_sNom) ' Dupont
cEntites.Add oClient1
Dim oClient2 As New clsClient
oClient2.IInfoIdentite_sNom = "Etdupont"
Set oClient2.IInfoIdentite_oAdresse = New clsAdresse
cEntites.Add oClient2
Dim oFournisseur1 As New clsFournisseur
oFournisseur1.IInfoIdentite_sNom = "global factory"
' Test spécifique modification : le nom reste global factory
oFournisseur1.IInfoIdentite_sNom = "global factory2"
' Debug.Print (oFournisseur1.IInfoIdentite_sNom) ' Erreur car inaccessible
Set oFournisseur1.IInfoIdentite_oAdresse = New clsAdresse
oFournisseur1.IInfoProfessionnelle_sSiret = "123456784512"
oFournisseur1.QteMinimaleParArticle = 10
cEntites.Add oFournisseur1
Dim oFournisseur2 As New clsFournisseur
oFournisseur2.IInfoIdentite_sNom = "supplier"
Set oFournisseur2.IInfoIdentite_oAdresse = New clsAdresse
oFournisseur2.IInfoProfessionnelle_sSiret = "123456784514113"
cEntites.Add oFournisseur2
' Ajout de commandes
Dim dictCommandeClient1_1 As New Scripting.Dictionary
dictCommandeClient1_1.Add "Casquette", 1
dictCommandeClient1_1.Add "Crème solaire", 5
oClient1.ICommande_AjouteCommande dictCommandeClient1_1
Dim dictCommandeClient1_2 As New Scripting.Dictionary
dictCommandeClient1_2.Add "Parasole", 1
oClient1.ICommande_AjouteCommande dictCommandeClient1_2
Dim dictCommandeFournisseur1_1 As New Scripting.Dictionary
dictCommandeFournisseur1_1.Add "Casquette", 1
dictCommandeFournisseur1_1.Add "Crème solaire", 5
oFournisseur1.ICommande_AjouteCommande dictCommandeFournisseur1_1
Dim dictCommandeFournisseur2_1 As New Scripting.Dictionary
dictCommandeFournisseur2_1.Add "Parasole", 1
oFournisseur2.ICommande_AjouteCommande dictCommandeFournisseur2_1
' Affichage en tirant parti du polymorphisme
Dim vEntite As Variant
For Each vEntite In cEntites
vEntite.IAffichable_Affiche
Next vEntite
End Sub
Fenêtre d’exécution (CTRL + G)
_________ Client : Dupont _________
Adresse :
Commandes :
CLIENT00000001
1 X Casquette
5 X Crème solaire
CLIENT00000002
1 X Parasole
________________________
_________ Client : Etdupont _________
Adresse :
Aucune commande enregistrée
________________________
_________ Fournisseur : global factory _________
Adresse :
N° siret : 123456784512
Qté minimale / article : 10
Commandes :
ACHAT00000001
10 X Casquette
10 X Crème solaire
________________________
_________ Fournisseur : supplier _________
Adresse :
N° siret : 123456784514113
Qté minimale / article : 0
Commandes :
ACHAT00000002
1 X Parasole
________________________
C’est déjà la fin de ce billet.
Au fil de celui-ci, nous avons vu comment définir et utiliser des interfaces en VBA.
C’est une notion supplémentaire qu’il peut être intéressant de connaître afin de construire des projets plus faciles à faire évoluer.
À bientôt !
Quelques ressources :
- La documentation
- Cette page