Developpez.com - Rubrique Excel

Le Club des Développeurs et IT Pro

Programmation Orientée Objet en VBA

Le 2008-10-30 20:41:55, par =JBO=, Membre émérite
Bonjour,

En fait le fil de messages originel est à rechercher sur le forum Access/VBA Access qui évoquait la problématique de l'héritage pour les classes développées en VBA: Héritage, possible ou non?

Donc la discussion a quelque peu dévié sur la question du polymorphisme, et s'est retrouvé déplacée sur le forum Général VBA.

_____---====OOOOO====---

Au risque de déborder un peu sur le sujet originel, je souhaite préciser comment on met en œuvre le polymorphisme en VB/VBA.

Envoyé par Pierre Fauconnier
Ce ne serait pas l'inverse ? ... Implements permet "une sorte" d'héritage, mais pas le polymorphisme... (enfin, c'est ce qu'il me semble...)
D'abord il faut s'entendre sur la signification de polymorphisme.
Aussi, je me reposerai sur Wikipédia avec un court extrait ci-dessous et la suite sur la page de Wikipédia illustrée par un exemple:
Envoyé par Wikipédia

En informatique, le polymorphisme est l'idée d'autoriser le même code à être utilisé avec différents types, ce qui permet des implémentations plus abstraites et générales.
Je reprends l'exemple de la page de Wikipédia (merci de vous y reporter) en l'adaptant au VB/VBA.

Une classe-interface cForme décrit les méthodes à implémenter pour toutes les "formes" géométriques:
Code :
1
2
3
4
5
Option Explicit

Public Function Aire() As Double
    Aire = 0
End Function
Une classe cCarré décrit les formes de type Carré et implémente les méthodes d'une "forme" géométrique.

L'utilisation du mot-clé Implements (dans la partie déclaration du module de classe) spécifie qu'une classe implémente l'interface publique (méthodes et propriétés) d'une autre classe.
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Option Explicit

Implements cForme

Private p_nCôté As Double

Private Function cForme_Aire() As Double
    cForme_Aire = p_nCôté * p_nCôté
End Function

Public Property Get Côté() As Double
    Côté = p_nCôté
End Property

Public Property Let Côté(n As Double)
    p_nCôté = n
End Property
Une classe cCercle décrit les formes de type Cercle et implémente les méthodes d'une "forme" géométrique:
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Option Explicit

Implements cForme

Private p_nRayon As Double

Private Function cForme_Aire() As Double
    cForme_Aire = CDbl(3.1415926535) * p_nRayon * p_nRayon
End Function

Public Property Get Rayon() As Double
    Rayon = p_nRayon
End Property

Public Property Let Rayon(n As Double)
    p_nRayon = n
End Property
Dans un module de code, on spécifie la fonction AireTotal() qui reçoit en paramètre une collection d'objets "formes" et retourne la somme des aires de ces objets.
C'est ici que le polymorphisme rentre en jeu puisque, selon le type de "forme", la fonction fait toujours appel à la bonne méthode de calcul Aire(). Tout type de "forme" géométrique qui implémente la classe/interface cForme peut être ajouté sans nécessiter une modification de la fonction AireTotal().
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
Public Function AireTotal(collFormes As Collection) As Double
    Dim oForme As cForme
    Dim nTotal As Double
    
    nTotal = 0
    
    For Each oForme In collFormes
        ' la fonction "sait" automatiquement quelle méthode Aire() appeler
        nTotal = nTotal + oForme.Aire()
    Next oForme
    
    AireTotal = nTotal
End Function
Enfin, on peut aussi coder une procédure pour tester tout ça:
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Public Sub TestFormes()
    Dim oCarré As New cCarré
    Dim oCercle As New cCercle
    Dim collFormes As New Collection
    Dim nTotal As Double
    
    oCarré.Côté = 3.4
    oCercle.Rayon = 5.1
    
    collFormes.Add oCarré
    collFormes.Add oCercle
    
    nTotal = AireTotal(collFormes)

    Debug.Print nTotal
End Sub
A l'usage, l'implémentation d'interface permet:
(1) de coder des procédures "génériques" qu'il ne sera pas nécessaire de modifier pour prendre en compte de nouveaux "sous-types" (exemple la fonction AireTotal()),
(2) d'obtenir un code plus robuste parce que les appels de méthodes sont vérifiés à la compilation (liaison dynamique "late binding" mais avec la rigueur du contrôle de type).

Merci aux courageux et curieux qui ont lu jusqu'au bout.
_
  Discussion forum
27 commentaires
  • Pierre Fauconnier
    Responsable Office & Excel
    Salut...

    Bah... Tant pis pour le débordement...

    Dans les exemples que tu proposes, je ne vois pas de polymorphisme (... le même code à être utilisé avec différents types...) puisque les classes Carré et Cercle utilisent le même type (même (absence de) paramètre..., même type renvoyé)...
    Pour moi, le polymorphisme, c'est d'avoir deux fonctions de même nom dans une même classe, mais avec la possibilité de passer des paramètres différents
    Code :
    1
    2
    3
    4
    5
    6
    7
    function Aire(Cote as double) as double
        ...
    end function
    
    function Aire(Longueur as double, Largeur as double) as double
        ....
    end function
    Ce cas est impossible en vba car pas de polymorphisme, alors qu'il est possible en vb.net.

    Donc, pour moi, dans ton exemple, on met bien un "pseudo"- héritage dont je ne vois par ailleurs pas vraiment l'intérêt...
  • =JBO=
    Membre émérite
    Envoyé par Pierre Fauconnier

    Cela étant, quel est l'intérêt de ce type de polymorphisme en VBA? Dans la pratique, je ne vois pas trop en quoi cela peut aider, si ce n'est à "généraliser" des propriétés "communes" à différentes classes. Si l'intérêt s'arrête là, je trouve que c'est un peu lourd à mettre en place, non?

    Ton avis m'intéresse. Merci
    Tout d'abord, l'implémentation d'interface est un des mécanismes fondamentaux de l'architecture COM de Windows, permettant l'intégration de nouveaux types d'objets à l'intérieur de cette infrastructure complexes.
    C'est toujours ce même mécanisme qui permet d'étendre les fonctionnalités de l'IDE VB/VBA.

    Pour ma part, je m'en suis beaucoup servi dans le cadre de projets complexes en VBA/Access.
    En effet, il y a un moment où un grand nombre de formulaires rend difficile (problématique) l'exécution d'une application Access.

    J'ai donc "sorti" et factorisé un maximum de codes communs aux formulaires pour coder des fonctions/procédures polymorphes.
    Ceci m'a amené à spécifier une classe-interface cFormApp listant les méthodes qu'un formulaire devait implémenter (le mot-clé Implements est autorisé dans le module de code d'un formulaire ou d'un état).
    Cette classe-interface était alors utilisée dans mes fonctions/procédures polymorphes.

    Ensuite dans les applications Access, j'ai rattaché les fonctions polymorphes aux barres de menus et d'outils. La première tâche de la fonction polymorphe est de récupérer l'objet en cours (le formulaire qui a le focus) puis d'y appliquer le reste du code.

    Cette méthode m'a permis:
    (1) de développer des gros projets qui se seraient essoufflés à cause du volume formulaire/code (nombre de formulaires divisé par 3 voire 4),
    (2) de développer dans de bonnes conditions grâce à une meilleure robustesse du code,
    (3) de gérer au mieux l'évolution des applications:
    ___(3.a) rapidité pour l'ajout d'un nouveau type de formulaire,
    ___(3.b) ajout de nouvelles fonctionnalités (au niveau de la classe-interface) en limitant les risques d'oublis (la compilation n'étant pas possible sans l'implémentation complète au niveau des formulaires).

    Je trouve que c'est lourd à mettre en place pour de petits projets.
    Cela pourrait quand même se justifier si on développe du code réutilisable dans de nombreux projets.

    En revanche dans des projets "larges" la question de la lourdeur est vite relativisée et cette méthode de travail procure des gains notables.
    _
  • =JBO=
    Membre émérite
    Bonjour,
    Envoyé par mout1234

    Je découvre par hasard ce post avec beaucoup d'intérêt.
    Je crois aussi que c'est une fonctionnalité digne d'intérêt mais elle implique de se "casser la tête" au moment de la conception (ou re-conception) d'une application.

    Envoyé par mout1234
    JBO, deux questions stp:

    • depuis quand (quelle version Office) existe l'instruction Implements?
    L'instruction Implements est opérationnelle dans Office depuis la version 2000.

    Envoyé par mout1234
    • Peux-tu citer quelques exemples de méthodes de ton interface cFormApp, histoire de mieux situer l'avantage de cette méthode de dev qui me plait bien ?
      Je ne vois pas bien notamment ce qui peut ainsi te permettre de diminuer autant ton nombre de formulaires par le seul apport de cette interface.
    Pour ce qui est de la réduction miraculeuse du nombre de formulaires, j'ai fait un raccourci dans mon explication.
    En fait, en mentionnant cet effet bénéfique, je voulais juste témoigner de l'effet-levier qu'on est en droit d'attendre d'une conception qui s'appuie sur le principe d'implémentation d'interface.

    Maintenant, je peux donner quelques explications et éclaircissements pour montrer qu'il ne s'agit pas juste de "paroles en l'air".

    En fait, il s'agissait d'une application MDI développée avec Access 2000 et qui permettait de suivre de nombreux dossiers avec de nombreuses étapes, elles-mêmes découpées en sous-étapes.
    Pour chaque sous-étape, on mettait en œuvre des fonctionnalités (commandes -> code VBA) propres à la sous-étape.

    Etape 1. Un formulaire par sous-étape
    Initialement, il y avait autant de formulaires que de sous-étapes, mais l'application devenait instable à cause de leur grand nombre.
    En outre, la modification d'une étape (exemple ajout de nouvelles informations) pouvait impliquer la modification de plusieurs formulaires sous-étape.

    Etape 2. Un formulaire par étape permettant de gérer toutes les sous-étapes
    Ensuite, nous avons essayé de gérer toutes les sous-étapes d'une étape dans un seul formulaire (instancié avec l'instruction New), mais le volume de code et le nombre de données à afficher rendait problématique les modifications du formulaire (plantage de l'IDE, sauvegarde d'une durée interminable, code corrompu...).

    N.B. La remarque qui suit ne vaut que pour Access:
    En fait, nous avons constaté qu'il fallait absolument éviter d'avoir beaucoup de code dans le module de code d'un formulaire.

    Etape 3. Restructurer l'application, implémentation d'interfaces
    Donc, le problème était de (1) limiter le nombre de formulaires, (2) sortir le code des commandes d'un formulaire pour le placer dans un module de code indépendant.

    Pour atteindre cet objectif, nous avons (1) simplifié chaque formulaire d'étape avec implémentation d'une interface, (2) déplacé le code de gestion des sous-étapes dans des modules de code de classe distincts qui eux-mêmes implémentent une interface (le contexte informationnel et fonctionnel).
    Selon la complexité des étapes/sous-étapes, toutes les étapes pouvaient être soit regroupées en un seul module de classe, soit détaillées en autant de modules de classe.

    C'est ainsi que le nombre de formulaire a été divisé par 4 et que le volume et la complexité du code ont été répartis dans des classes implémentant les fonctionnalités.
    Il s'agit donc d'une architecture logicielle applicative complexe, à 2 niveaux d'interface... Maintenant, on comprend mieux pourquoi j'ai fait un raccourci dans mon explication précédente.

    Je donnerai des exemples concrets dans un prochain post.
    _
  • =JBO=
    Membre émérite
    Comme promis, voici un exemple "assez simple" d'implémentation d'interface.
    L'exemple est appliqué à Access, mais ce serait aussi "facile" avec Excel, ou autres...

    Dans cet exemple, l'objectif est d'ajouter dans la barre de menu d'Access un nouveau menu dont la liste d'options est dynamiquement adaptée en fonction de l'objet actif (l'objet fenêtré qui détient le focus).

    * La légende (Caption) du nouveau menu est: "Spécial implémentation".

    * Ce menu peut travailler avec différents types d'objets: Form, Report et Datasheet (Table ou Query).

    La vue synthétique ci-dessous illustre la faculté de ce menu à s'adapter à l'objet actif en affichant une liste spécifique d'options de menu.



    Avec une interface, il est possible de spécifier les modalités d'une coopération entre le menu et l'objet actif.
    On va simplement énoncer toutes les fonctionnalités à implémenter pour les classes d'objet qui déclareront s'y conformer.

    Évidemment, la conception d'une interface n'est pas quelque chose de trivial, ni d'instantané.

    * Il faut d'abord réfléchir aux interactions entre menu et objets:
    ____ menu <=> objet de type Form
    ____ menu <=> objet de type Report
    ____ menu <=> objet de type Datasheet

    * Avec chacune de ces situations spécifiques, menu et objets sont trop fortement couplés; leur réutilisation est difficile; la prise en compte d'un nouveau type d'objet demande toujours autant d'effort, voire même plus d'effort.
    Ici le menu "connait" la manière de travailler qui convient selon qu'il s'agit d'un Form, d'un Report, d'un Datasheet... Conséquence: taille volumineuse du code; complexité du code.

    * Grâce à l'interface, il est relativement aisé d'insérer une couche d'abstraction qui procure le découplage souhaité entre menu et objets:
    ____ menu <= interface
    ____________ interface => objet de type Form
    ____________ interface => objet de type Report
    ____________ interface => objet de type Datasheet
    Le menu utilise l'interface: il sait comment travailler avec tout objet conforme à l'interface.
    Une classe d'objet implémente l'interface: la conception de la classe de l'objet est simplifiée car le "périmètre" est connu.

    Pour revenir à l'exemple, cette interface est définie au moyen d'un module de classe que j'ai appelé iMenuBarOptions.
    Avec ce nom, j'ai voulu donner un sens précis à l'interface l'interface est focalisée sur les options à afficher dans le menu et l'exécution des traitements correspondant.

    Par convention, dans le monde Windows les interfaces sont facilement identifiées par la première lettre de leur nom qui est toujours la lettre I... par similitude avec la première lettre du mot "Interface".

    En lisant l'interface iMenuBarOptions on apprend qu'une classe d'objet conforme doit pouvoir:
    * fournir un identificateur permettant d'associer étroitement l'objet actif et une liste d'options,
    * fournir la liste des options à afficher par le menu (ou modifier directement le menu),
    * fournir une référence (type Object du VBA) sur l'objet réel (objet actif), instance d'un Form Report, Datasheet...
    * à partir d'un clic souris sur une option affichée dans le menu, lancer l'exécution du code correspondant, implémenté dans l'objet actif.

    Module de classe iMenuBarOptions
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Option Explicit
    
    ' Pour empêcher d'utiliser un objet qui instancie directement l'interface,
    ' toutes les procédures/propriétés de l'interface déclenche une erreur récupérable standard.
    '   Erreur 445: L'objet ne gère pas cette action
    
    Public Property Get OptionsId() As String
        ' interface à implémenter
        Err.Raise 445, "iMenuBarOptions"
    End Property
    
    Public Function OptionsLister(oMenu As Office.CommandBarPopup, ByRef tabOptions_out() As String) As Boolean
        ' interface à implémenter
        Err.Raise 445, "iMenuBarOptions"
    End Function
    
    Public Property Get Object() As Object
        ' interface à implémenter
        Err.Raise 445, "iMenuBarOptions"
    End Property
    
    Public Sub OptionClick(oCtrl As Office.CommandBarControl, nIndex As Integer)
        ' interface à implémenter
        Err.Raise 445, "iMenuBarOptions"
    End Sub
    Je m'arrête là pour aujourd'hui, mais je pense développer les points suivants:
    (1) Comment le menu utilise-t-il l'interface iMenuBarOptions ?
    (2) Comment l'objet actif est-il référencé ?
    (3) Exemple d'implémentation de l'interface iMenuBarOptions (Form ou Report ou Datasheet ???)

    En attendant, je vous donne l'exemple complet en pièce jointe.
    C'est de l'Access 2000, les versions antérieures d'Access ne permettent pas l'implémentation d'interface.

    [EDIT] Merci à Maxence d'avoir signalé un bug... J'ai changé la pièce jointe. [/EDIT]
    _
  • =JBO=
    Membre émérite
    Envoyé par mout1234

    Je peine toutefois à imaginer que l'usage d'interface ait apporté à lui seul une diminution substantielle du code par rapport à de simples menus spécifiques à chaque type d'objet.
    L'usage d'interface permet-il de réduire le volume de code ?

    Oui et Non. Je m'explique:

    Non, dans l'absolu je ne pense pas que le volume de code "vraiment utile" s'en trouve réduit.

    Oui, parce qu'il favorise la factorisation de codes redondants et dupliqués.
    Mais la réduction du volume du code est plus le résultat d'une "bonne" conception ou re-conception, dont l'interface ne serait qu'une des modalités.

    Envoyé par mout1234
    J'ai plus la conviction que c'est ton architecture dans son ensemble, tirant entre autres partie des implémentations qui en est la source.
    Avec l'utilisation d'une interface, on est obligé de repenser et rationnaliser "la logique" des composantes d'une application.

    Dans l'exemple du menu "Spécial implémentation" on va distinguer 3 "logiques":

    (1) La logique de gestion des options d'un menu qui est concrétisée par les procédures Menu_OnAction() et Option_OnAction().
    Cette logique est unique car étroitement liée à une interface, ici iMenuBarOptions.
    Il n'est pas nécessaire de "comprendre l'environnement" et donc pas nécessaire d'avoir autant de "logiques" que d'objets actifs (Form, Report, Query, Table).

    (2) La logique d'accès à l'objet actif, concrétisée par la procédure MenuBarOptions().
    Cette logique exploite le contexte courant connu d'elle seule (ici l'environnement est perçu à travers l'objet Screen et la propriété CurrentObjectType).
    En retour, elle désigne un objet conforme à l'interface iMenuBarOptions, donc directement utilisable par la logique de gestion des options d'un menu.
    Cela peut faire penser au "pattern factory" qui sait fournir un objet opérationnel et du bon type (ou instance de la bonne classe) à une "logique commune" basée sur une interface (ou une classe de base).

    (3) La logique d'exécution des commandes liées aux options du menu.
    Ces commandes sont concrétisées par autant de procédures, codées directement dans le module de code de l'objet actif.

    _____---====OOOOO====---

    Je profite de l'occasion pour donner le code de la procédure MenuBarOptions() qui implémente la logique d'accès à l'objet actif (cf. le point 2).
    C'est cette logique qui devrait être modifiée si on souhaite adapter le menu "Spécial implémentation" à de nouveaux types d'objets.

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    Public Function MenuBarOptions() As iMenuBarOptions
        Static oActiveObject As Object
        Dim oForm As Access.Form
        
    On Error GoTo ProcErr
    
        Select Case Application.CurrentObjectType
        Case acTable, acQuery
            ' Une erreur est déclenchée si ActiveDatasheet inaccessible
            Set oForm = Screen.ActiveDatasheet
            
            ' Table et Query n'ont pas de module de code
            ' ==> créer dynamiquement un objet qui implémente l'interface
            Set oActiveObject = New cDataSheet
        
        Case acForm
            ' Trouver l'objet Form actif
            Set oActiveObject = Screen.ActiveForm
            
        Case acReport
            ' Trouver l'objet Report actif
            Set oActiveObject = Screen.ActiveReport
            
        Case Else
            ' Pas d'objet actif "exploitable"
            Set oActiveObject = Nothing
            
        End Select
        
        ' Accéder à l'interface iMenuBarOptions de l'objet actif
        ' Une erreur est déclenchée si l'objet actif n'implémente pas l'interface
        Set MenuBarOptions = oActiveObject
    
        Exit Function
        
    ProcErr:
        Set MenuBarOptions = Nothing
        
    End Function
    Si je veux mettre en œuvre dans Excel le menu "Spécial implémentation", je conserve la logique (1) et j'adapte la logique (2).
    _
  • =JBO=
    Membre émérite
    Bonjour,
    Envoyé par Pierre Fauconnier

    Pour moi, le polymorphisme, c'est d'avoir deux fonctions de même nom dans une même classe, mais avec la possibilité de passer des paramètres différents
    [...]
    Ce cas est impossible en vba car pas de polymorphisme, alors qu'il est possible en vb.net.

    Donc, pour moi, dans ton exemple, on met bien un "pseudo"- héritage dont je ne vois par ailleurs pas vraiment l'intérêt...
    En fait tu évoques la technique de surcharge de fonction (ou d'opérateur) qui n'est pas particulièrement liée aux langages à/orientés objet. On peut aussi la retrouver dans certains langages fonctionnels ou procéduraux.

    Tu assimiles «surcharge de fonction» et «polymorphisme», et c'est une confusion que l'on voit fréquemment sur l'internet.
    Dans Wikipédia en anglais l'article sur le polymorphisme expose les choses plus précisément en distinguant différentes formes de polymorphisme:
    • ad-hoc polymorphism,
    • parametric polymorphsim d'où découle le subtyping polymorphism (ou encore inclusion polymorphism).


    La surcharge de fonction relève de l'ad-hoc polymorphism.

    L'exécution dynamique de méthode relève du subtyping polymorphism (c'est l'exemple que j'ai donné).

    Une différence essentielle entre surcharge et exécution dynamique peut s'expliquer quand on considère l'étape de compilation d'un programme.

    En ce qui concerne la surcharge, un compilateur peut déterminer la fonction à appeler au moyen des types des paramètres qui sont déjà connus à la compilation: la liaison est établie dès la compilation du code.

    En ce qui concerne une "fonction polymorphe" (comme la fonction AireTotal()) le compilateur ne peut pas connaître la bonne méthode (ici la méthode Aire()) qui sera exécutée car le type réel (la "forme" géométrique) ne sera connu qu'à l'exécution.
    _
  • Pierre Fauconnier
    Responsable Office & Excel
    Salut...

    D'accord avec toi. En faisant des tests, j'ai compris mon mélange entre les deux notions... Merci pour les explications détaillées.

    Cela étant, quel est l'intérêt de ce type de polymorphisme en VBA? Dans la pratique, je ne vois pas trop en quoi cela peut aider, si ce n'est à "généraliser" des propriétés "communes" à différentes classes. Si l'intérêt s'arrête là, je trouve que c'est un peu lourd à mettre en place, non?

    Ton avis m'intéresse. Merci
  • mout1234
    Membre expert
    Salut,

    Je découvre par hasard ce post avec beaucoup d'intérêt.

    JBO, deux questions stp:

    • depuis quand (quelle version Office) existe l'instruction Implements?
    • Peux-tu citer quelques exemples de méthodes de ton interface cFormApp, histoire de mieux situer l'avantage de cette méthode de dev qui me plait bien ?
      Je ne vois pas bien notamment ce qui peut ainsi te permettre de diminuer autant ton nombre de formulaires par le seul apport de cette interface.
  • mout1234
    Membre expert
    Bonjour,

    Merci JBO pour ces explications très détaillées.

    Je devine que cette approche vaut surtout pour de gros développements, ou tout au moins des projets pour lesquels un temps important est consacré à la conception de l'application.

    Pour ma part, je suis plus souvent dans des contextes de projets où il faut développer pour hier des fonctionnalités dont on a à peine pris le temps d'en définir les specs...

    Mais je suis convaincu par tes commentaires que cela doit valoir le coup, même pour des petites applis, en terme d'évolutivité et de lisibilité du code.... reste à me trouver le temps pour expérimenter cela...

    Merci encore et bon Implementations
  • -={-_-}=-
    Membre régulier
    Bonjour,

    Merci JBO pour tous ces éclaircissements.
    En tant que développeur Excel, j'utilise très peu de forms lors de mes dev.
    Mais par contre je vois aisément l'extension du concept au module de classe.
    Ce m'évitera de les reprendre tous si j'ai un nouvel objet à rajouter.
    Donc beaucoup moins de maintenance!!!!

    J'appliquerais tes conseils pour mes futurs dev.

    JBO