V comme Visiteur | 1 vote(s)
Par Emmanuel Deloget, dimanche 13 août 2006 à 13:30 :: Architecture Orientée Objet :: permalien #8
Tags: composition, design pattern, héritage, OCP
Non, ce n'est pas le titre d'une série télévisée, la discussion qui s'en suivrait si tel était le cas n'aurait de toute façon aucune espèce d'intérêt dans le cadre d'un billet parlant d'architecture orientée objet. Le visiteur auquel je fais référence est bien évidemment le patron de conception du même nom.
Avant de discuter plus avant sur ce patron, et parce que les patrons de conception sont encore des entité méconnues, une brève description s'impose.
La définition usuelle de ce parton de conception est la suivante : il représente une opération à effectuer sur les élements d'une structure d'objet. Il permet en outre de définir de nouvelles opérations sans changer les classes des éléments sur lesquels il opère.
La représentation UML du patron fait intervenir plusieurs classes : AbstractVisitor définit l'interface du visiteur comme un ensemble de méthodes visit() prenant des paramètres de type différent. Les classes ConcreteVisitorA et ConcreteVisitor2 implémentent ces méthodes. AbstractElement définit la méthode abstraite accept(), qui est implémentée dans ConcreteElement1 et ConcreteElement2.

Gràce au mécanisme de surcharge, ConcreteElement1::accept() va appeler la méthode AbstractVisitor::visit() qui gère les objets du type ConcreteElement1, tandis que ConcreteElement2::accept() va appeler celle qui gère les objets du type ConcreteElement2.
class ConcreteElement1
{
public:
virtual void accept(AbstractVisitor& visitor)
{
// ce code va appeler
// AbstractVisitor::visit(ConcreteElement1& element)
visitor.visit(*this);
}
};
class ConcreteElement2
{
public:
virtual void accept(AbstractVisitor& visitor)
{
// ce code va appeler
// AbstractVisitor::visit(ConcreteElement2& element)
visitor.visit(*this);
}
};
Le mécanisme mis en place s'appelle double dispatch et permet d'éviter l'utilisation du transtypage dynamique, méthode plus lourde et difficilement extensible.
L'utilisation typique du patron Visiteur consiste à parcourir une liste d'objets du même type de base pour y appliquer un algorithme dépendant de ce type de base. Par exemple, je peux imaginer une classe CarElement dont vont hériter les classes Wheel, SteeringDevice et Engine. L'objet Car contient une liste de CarElement, et je souhaite par exemple compter le nombre d'éléments de type Wheel. Pour ce faire, je crée un visiteur qui implémente visit(Wheel&) de manière à incrémenter un compteur; les autres méthodes visit() ne font rien. En itérant sur la liste de CarElement, je suis maintenant à même de compter le nombre de roues de mon véhicule, sans pour celà avoir été obligé de modifier quoi que ce soit dans la hiérarchie des objets utilisés.
On le voit, ce patron a de grande qualités. Ce qu'on discerne mal, ce sont ses défauts, qui sont pourtant particulièrement génants.
En fait de défauts, le plus important est aussi le plus visible : le patron de conception Visiteur introduit une dépendance circulaire dans la conception. ConcreteElement1 se doit de connaitre l'existence de AbstractVisitor qui doit connaitre ConcreteElement1. Bien souvent, les dépendances circulaires sont le signe d'un problème de cohérence face aux principes fondamentaux de l'architecture objet[1], et c'est bien évidemment le cas ici : le principe ouvert-fermé (Open/Closed Principle, où OCP) n'est pas respecté. Ce principe stipule que mes classes doivent être ouvertes à l'extension mais fermées aux modifications, c'est à dire qu'une évolution logicielle doit pouvoir ajouter de nouvelles fonctionalités sans modifier le code existant. C'est bien évidemment impossible avec le visiteur : si j'ajoute un nouveau type d'objet à visiter, je dois modifier mon visiteur abstrait et toutes les classes dérivées de AbstractVisitor vont s'en trouver impactée.
Robert C. Martin propose une solution à ce problème, en utilisant ce qu'il nomme des visiteurs acycliques (pdf). Dans un visiteur acyclique, AbstractVisitor est une classe dégénérée - c'est à dire qu'elle ne contient aucune méthode. De cette classe dérive AbstractElement1Visitor ou AbstractElement2Visitor qui définissent les méthodes visit() correspondante (une seule par classe). ConcreteElement1, dans sa méthode accept() vérifie si l'objet visitor qu'il reçoit en paramètre est bien du type AbstractElement1Visitor.
class AbstractVisitor
{
};
class AbstractElement1Visitor : public AbstractVisitor
{
public:
virtual void visit(ConcreteElement1& element) = 0;
};
class ConcreteElement1
{
public:
virtual void accept(AbstractVisitor* visitor)
{
AbstractElement1Visitor *el1visitor;
el1visitor = dynamic_cast<AbstractElement1Visitor>(visitor);
if (el1visitor) {
el1visitor->visit(*this);
}
}
};
Cette solution est relativement satisfaisante - à partir du moment où le langage objet utilisé permet de connaitre de manière dynamique le type d'un objet (en C++, dynamic_cast<> remplit cet office).
On notera aussi que les langages ayant des possibilités de reflection et d'introspection peuvent permettre l'implémentation de solutions similaires, quoique souvent plus élégantes. D'autres langages (comme Nice) permettent de définir des multiméthodes, qui offrent une alternative satisfaisante à la technique du double dispatch.
Utiliser le patron Visiteur tel quel est certes très utile, mais cette utilisation doit être balancée par le fait qu'il ne respecte pas OCP. Si le nombre de classes concernées est faible, le problème n'est pas très important. Dans le cas contraire, il peut être intéressant d'évaluer la possibiliter d'utiliser des solutions alternatives bien que le code résultant soit souvent moins idiomatique (et donc plus difficile à déchiffrer).
Notes
[1] je me permet d'introduire les principes fondamentaux de l'architecture objet ici, mais je prendrais soin de les discuter plus tard, dans une série de billets dédiés
Commentaires
1. Le samedi 14 octobre 2006 à 05:47, par Karim Refeyton
2. Le samedi 14 octobre 2006 à 05:50, par Karim Refeyton
3. Le lundi 16 octobre 2006 à 22:34, par Emmanuel Deloget
4. Le dimanche 22 octobre 2006 à 18:35, par Karim Refeyton
5. Le mardi 24 octobre 2006 à 16:10, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire