Les visiteurs, une question de nommage, et le double-dispatch

Une histoire qui commence mal

OK, je tranche le malheureux pattern Visiteur a la vie dure; on ne l’aime pas trop, il est mal compris, et le pauvre est sous utilisé. Alors bon même s’il a ses défauts, pourquoi lui en vouloir autant, alors qu’il apporte justement ses avantages au code orienté objet.

Et oui vous avez bien lu orienté objet. Jusqu’à aujourd’hui j’ai vu du code qui ressemble à ça?

  1. On a soit des objets très complexes, avec des comportements qu’il n’est pas forcément intéressant de mettre dans l’objet même. Le code ci-dessous montre un objet ou les méthodes qui permettent de récupérer les livres d’un certain genre ne sont pas forcément appropriées dans cette partie du code. Pourquoi parce qu’il est envisageable (selon le bon sens) que d’autres genres serait apprécié. Et s’il faut ajouter d’autres méthodes encore.

  2. Ou alors on a des objets anémiques (cf Martin Fowler) et le comportement est bien en dehors des objets traités, mais, et c’est la ça pèche, le comportement est délocalisé dans des helpers. Bref en gros c’est de la programmation procédurale, ce sont des structures qui sont manipulées par des fonctions, c’est du C avec des espaces de nommage (les classes *Helper.java). La programmation objet en prends un coup, pas étonnant que les principes objets ne marchent pas dans ce contexte, mais je diverge. Bref on a du code qui ressemble à ce qui suit. Un objet anémique qui ne fait rien. Au mieux il aura probablement les méthodes equals et hashCode et peut-être un toString.

    Et le démoniaque helper :

Comme vous le voyez les deux exemples ci-dessus ne sont pas vraiment élégants, même si je préfère la première voie. A long terme ce n’est probablement pas une bonne idée. J’aimerais d’ailleurs avoir l’avis des gens du DDD?

Et c’est là que notre ami le visiteur va nous aider.

Pourquoi le visiteur nous aide, qu’apporte-t-il ?

Bonne question, ce pattern est souvent incompris, et pour cause, il ne porte pas un nom qui lui facilite la vie.

Et oui pour le coup un visiteur n’est pas fait pour visiter. Page 387 de la traduction française du livre Design Patterns (par le GoF), nous pouvons lire :

Le visiteur fait la représentation d’une opération applicable aux éléments d’une structure d’objet. Il permet de définir une nouvelle opération, sans qu’il soit nécessaire de modifier la classe des éléments sur lesquels il agit.

Effectivement aussi, ce livre donne comme un exemple un arbre. Et le visiteur prends toute sa puissance sur un arbre ou sur une structure composite. Mais ce n’est le seul cas ou celui-ci est utile, dans tous les cas il s’agit bien de permettre l’ajout / la suppression / la modification de comportements d’une manière objet sans retoucher à ce qui existe déjà. Je le répète le fait que le visiteur marche super bien sur un arbre est un bonus, mais le problème adressé, l’intention du visiteur n’est pas de visiter, mais de définir une nouvelle opération sans changer l’existant sur lequel il agit.

Il faut mesurer l’intérêt du visiteur suivant deux axes.

  1. S’il y a beaucoup d’objet du domaine qui peuvent avoir le même comportement, ou si la grappe de nœud d’un arbre est importante, un ou des visiteurs sera une bonne solution de conception pour mutualiser du code.
  2. S’il n’y a pas énormément d’objet du domaine, voir qu’un seul, mais que les comportements relatifs sont à la fois divers et volatiles. Alors le visiteur est un candidat pour ajouter des comportements sans faire de satané helper et sans avoir à modifier les éléments du domaine.
  3. Si vous avez des opérations différentes et un arbre ou des objets composite, le visiteur est le pattern pour vous, c’est la qu’il prendra toute son essence.
  4. Si finalement vous n’avez pas beaucoup de comportement, qu’ils ne risque pas beaucoup de bouger et que vous n’avez pas des objets variés pour mutualiser ce code, alors le visiteur n’est probablement pas pour vous.

Egalement aussi le visiteur étant un objet permet de conserver un état, ce que ne permettent pas les objets même du domaine ou les helpers (sauf si on utilise des objets contextes passé de fonction en fonction, ce n’est pas exceptionnel).

Exemple sans prétention de visiteurs

D’abord la grappe d’objet “complète” :

Et la partie relatives aux visiteurs, d’abord l’interface (ou j’ai choisi volontairement de ne pas mettre les mot Visitor et visit) :

Et voilà on des comportements différents liés à un objet en particulier, pas besoin de retoucher notre élément. Et on a une manière élégante de sortir nos comportements. Bien entendu, ce genre de chose est à faire avec du bon sens, en fonction du contexte et de l’opération à effectuer.

Quand on a davatage d’objets du domaine à visiter, attention!

Attention quand même, comme précisé plus haut, le visiteur n’est pas non plus sans défaut. Sur une structure d’objet profonde ou large, votre pattern visiteur va créer une dépendance cyclique entre lui et les objets sur lesquels il est sensé s’appliquer.

Si mon visiteur doit par exemple travailler sur plusieurs sous type de l’objet (on pourrait typiquement avoir ce genre de problème avec les structures composites) :

On voit vite le problème ou le visiteur est forcé d’implémenter des opérations pour des objets qui ne l’intéresse pas forcément. Le problème est contournable en utilisant intelligemment les interfaces, mais cette solution palliative a également des limites; on ne va faire implémenter 45 interfaces à nos objets.

Pour cela il y a une solution un peu plus complexe qui est également un pattern, c’est le Visiteur Acyclique. Je n’approfondie pas trop, mais l’idée est d’avoir pour chaque sous type du domaine une interface de visiteur qui permet de vérifier que l’instance du visiteur est acceptable. Evidemment vous pourrez adapter le comportement, et vous n’êtes non plus obligé d’implémenter toutes les méthodes, c’est le but de ce pattern acyclique.

Et typiquement le code du accept pour chaque sous-type de collection aurait une tête du genre :

Et voilà on a cassé les dépendance, et on est pas obligé d’implémenter toute les interfaces de chaque type de collection.

Le double dispatch, à ne pas confondre avec un visiteur

Le lecteur avertit aura vite deviné que ça ressemble au pattern stratégie, et il aura raison, ce sont des patterns comportementaux. Mais là ou le visiteur se distingue, et notamment dans des langages comme Java, .Net, C++ c’est qu’il utilise la technique du double dispatch.

Alors le double dispatch (double répartition) c’est quoi exactement, c’est un moyen pour le logiciel de résoudre au runtime les méthodes à exécuter.

Je vais citer les exemples wikipédia et transformer leurs exemples en Java.

On a donc deux catégories d’objets, des astéroïdes et des vaisseaux spatiaux.

Ok, maintenant dans le code on a ça

Comme en java c’est la méthode de l’instance qui est appelée, pas de problème pour nos astéroïdes. Mais là ou ça coince c’est au niveau des vaisseaux spatiaux. Les deux appels vont afficher sur la sortie sandard:

En effet le type réel du vaisseau spatial n’est pas connu, sauf si on fait de la reflection avec un instanceof, mais il y a plus élégant, c’est le double dispatch.

Si maintenant nos vaisseaux spatiaux ont tous les deux cette méthode définie :

Maintenant notre code utilisera l’API de cette façon :

Et on aura le code correcte utilisé.

Cette technique est utilisée par le visiteur, mais nous ne somme pas obligé d’avoir des visiteurs pour l’utiliser (la preuve par l’exemple grâce à wikipédia). C’est utilisé régulièrement dans la JDK, typiquement pour la sérialisation (même si c’est caché). Coté performance si on a le choix, le double dispatch sera toujours plus rapide qu’un instanceof. Coté design c’est pratique quand on a des branches d’objets qui travaillent ensemble.

Certains langages proposent nativement un support pour ces problèmes de résolution de type d’opérande, comme Nice.

A regarder aussi, c’est le multi dispatch ou les multi-méthodes, il y a notamment une implémentation de Rémy Forax de l’université de Marne-la-Vallée, cette implémentation a le mérite d’être standard Java, c’est à dire qu’elle n’étends pas le langage lui-même.

Pour y jeter un œilhttp://www-igm.univ-mlv.fr/~forax/works/jmmf/index.html

Récapitulatif sur le visiteur

Le visiteur est bien un ami, mais comme tous les potes, il ne sait pas tous faire non plus.

Un visiteur sait parcourir des arbres, il se débrouille super bien avec, mais il est aussi utile quand il n’y a pas d’arbre.

Un visiteur sert avant tout à extraire des comportements lié à un structure d’objet qui bouge peu. La structure peut être plate, ou en profondeur (cela dit je privilégierait la composition à la lace de l’héritage).

Le visiteur utilise la technique du double dispatch, ne pas confondre les deux.

Le visiteur permet de respecter le SRP (Single Responsibility Principle).

Le visiteur aide à maintenir le CCP (Common Closure Principle), c’est une histoire de cohésion entre les classes qui sont regroupées dans un même package.

The classes in a package should be closed together against the same kind of changes. A change that affects a package affects all the classes in that package.

Bon voilà, le débat reste ouvert, si vous pensez que j’ai tort, que j’oublie un point important, ou pour autre chose, il y a les commentaires.

Références

http://www.objectmentor.com/omSolutions/oops_what.html

http://www.objectmentor.com/resources/articles/visitor.pdf

http://www.objectmentor.com/resources/articles/acv.pdf

http://www.artima.com/cppsource/top_cpp_aha_moments.html

http://butunclebob.com/ArticleS.UncleBob.VisitorVersusInstanceOf

http://www.javaperformancetuning.com/articles/ddispatch.shtml

2 thoughts on “Les visiteurs, une question de nommage, et le double-dispatch”

  1. Salut,

    Tu as une coquille dans ton exemple de code (méthodes CollideWith au lieu de collideWith).
    Pour ma part, je trouve le design pattern visiteur dangereux et redondant car nécessitant de redéfinir la fameuse méthode accept qui effectue le double dispatch (attention au effets de bord si on sous-classe …).
    De plus il lie fortement objet visité et visiteur ….

    1. Fixed, merci pour le “bugreport”

      En fait je suis d’accord, que la définition (et la redéfinition) d’une méthode accept n’est pas vraiment super, surtout lorsqu’on a une gamme importante d’objet qui acceptent des visiteurs. Mais malgré tout je trouve que ça reste le moindre mal pour un logiciel orienté objet.

      D’une manière générale, déjà je privilégierait la composition à l’héritage (à ce sujet n’oublions pas que l’héritage casse très facilement l’encapsulation si chère à l’OOP, bref quand on prévoit l’héritage il faut bien réfléchir à l’Open Closed Principle (OCP)). Donc si l’arbre d’héritage est peu profond, on peu s’en sortir.

      C’est tout à fait vrai, le visité et le visiteur sont liés et c’est normal d’avoir des classes qui fonctionnent ensemble, il s’agit bien de pouvoir externaliser (ou même de mutualiser) les comportements des objets visités (cf le livre du GoF et l’article de Scott Meyers).

      Il ne faut pas se voiler la face, pendant des années on a créé des applications avec des objets métier anémiques, et avec un comportement entièrement localisé dans des helper ou des services ça ressemblait plus à du script dans des objets (donc en clair on utilisait juste la fonction namespace des objets). Je ne dis pas que tout ça était absolument mal, mais il aurait fallu mieux gérer les responsabilités et les rôles, quelles sont les classes qui fonctionnent ensemble ou qui sont cohésives, quelles sont les classes qui bougent le plus souvent et comment. Bref faire du véritable objet, à ce titre j’attends beaucoup à ce titre du Domain Driven Design.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">