dans conception orientée objet

Pré-requis :

  • Notion de surcharge de méthode ;
  • Notion de polymorphisme.

Contexte :

Dans la conception de notre application, nous sommes amenés à manipuler des figures géométrique : un cercle et un triangle. On souhaite modéliser ces figures en objet et pouvoir utiliser le polymorphisme.

Première solution : sans le design pattern visitor

Fastoche ! Vous vous modélisez le problème et vous présentez votre résultat à Mauricette (souvenir de BTS !).

Modélisation UML

Figure 1

Il vous félicite et aimerait maintenant effectuer quelques opérations mathématiques. En l’occurrence, il souhaite connaître l’aire de chaque figure géométrique. Comment implémenter ce besoin ?

Vous y réfléchissez quelques secondes, tout au plus, et vous avez une idée : ajouter une méthode dans les classes qui se charge de calculer l’aire. Vous mettez en œuvre cette solution (avec les tests unitaires qui vont avec) et tout fonctionne comme prévu. Simple et efficace, rien ne vous fait peur et Mauricette le sait.

C’est pourquoi, il vous demande maintenant d’ajouter le calcul du périmètre. Ni une, ni deux, vous vous mettez au travail et décidez d’ajouter une nouvelle méthode. Vous ouvrez pas mal d’onglets avec les fichiers à modifier (classes et tests unitaires) et implémentez la méthode. À nouveau, tout fonctionne comme prévu. Fier de vous, vous présentez votre solution.

Par la suite, Mauricette décidera d’ajouter une nouvelle figure. Puis quand ce sera fait, il vous demandera d’ajouter de nouveaux calculs mathématiques. Le nombre de fichiers à modifier croitra et vos classes — comme vos tests unitaires — deviendront de plus en plus volumineuses, que vous vous noyez dans votre propre code. Maintenant, vous redoutez plus que tout la moindre demande d’ajout d’un nouveau calcul de Mauricette.

Le choix de conception ne convient plus et il est plus que nécessaire de le revoir rapidement.

Pourquoi ?

Au début de notre conception, celle-ci répondait parfaitement au problème, mais ce dernier a depuis évolué, à défaut de notre architecture qui exprime maintenant ses limites. Depuis le début, nous ajoutons régulièrement de nouvelles méthodes de calculs et peu de nouvelles figures. Chaque ajout nous oblige à modifier toutes nos classes et tests unitaires existants.

Les classes ont maintenant trop de responsabilités. Il faut donc penser à déléguer ces responsabilités grâce au design pattern visitor.

 

Deuxième solution : avec le design pattern visitor

Vous ne pouvez pas fuir indéfiniment Mauricette. Vous avez identifié le problème et vous avez demandé conseil auprès de vos collègues. L’un d’eux vous propose d’externaliser toutes les opérations dans de nouvelles classes qui n’auront qu’une seule et unique responsabilité. Cela alourdit certes votre architecture avec la création d’une multitude d’objets par rapport à l’existant, mais vous y gagnez largement en lisibilité et flexibilité.

Il vous oriente donc vers le design pattern visitor. Vous décidez d’en prendre connaissance et mettez cette solution en place :

( Pour faciliter la lecture du diagramme, je n’ai gardé que deux opérations : l’aire et le périmètre. )

Modélisation UML avec le design pattern visitor

Figure 2

 

Explications

Chaque opération (calcul d’aire, périmètre) a donné lieu à une nouvelle classe, à responsabilité unique :

  • AreaShapeOperation a la responsabilité de calculer l’aire d’une figure.
  • PerimeterShapeOperation a la responsabilité de calculer le périmètre d’une figure.

Ces deux classes héritent de la classe abstraite ShapeOperation, qui définit deux méthodes abstraites : calcul(...). Seul le paramètre les différencie. L’une attend un objet de type Circle et l’autre de type Rectangle. Nous utilisons ici le principe de surcharge de méthode pour simplifier l’envoi de message et donc l’utilisation du code.

J’insiste là dessus. C’est bien le type du paramètre passé à la méthode qui déterminera son comportement (i.e, si c’est un type Rectangle, on aura le comportement de la méthode calcul(Rectangle)). Soyez attentif à ce détail car toute la complexité et la subtilité de ce design pattern repose là dessus.

Conséquences

Du coté de nos figures géométriques (cf figure 2), nous avons simplement rajouté une méthode appelée par convention accept(...). Cette méthode prend en paramètre un objet de type ShapeOperation et appelle la méthode  calcul(...) du visiteur (le paramètre operation) en lui passant en paramètre son instance (soit Rectangle, soit Circle).

Tout se fait maintenant de manière dynamique. Ajouter un nouveau calcul revient à ajouter une seule classe et d’y définir toutes les méthodes imposées par ShapeOperation. Idem au niveau des tests unitaires, une seule classe à définir. On gagne en flexibilité et en lisibilité. Adieu les milliers d’onglets dans votre éditeur de code !

 

Un peu de concret.

Je vous propose quelques morceaux de code en exemple pour conforter votre compréhension du design pattern visitor, qui se veut assez déroutant et peu intuitif.

Premier exemple avec l’aire :

// le visiteur (qui calcule l'aire)
AreaShapeOperation areaOperation = new AreaShapeOperation();

// la figure
Rectangle rectangle = new Rectangle();
Double result = rectangle.accept( AreaOperation );

Ce qui se passe dans la méthode accept(…):

// "operation" correspond à "areaOperation"
public Double accept( ShapeOperation operation ) {

    // Correspond à AreaShapeOperation.calcul(Rectangle)
    return operation.calcul( this );
}

La variable result contiendra le résultat du calcul, c’est-à-dire ici l’aire du rectangle.

 

Second exemple avec le périmètre :

// le visiteur (qui calcule le périmètre)
PerimeterShapeOperation perimeterOperation = new PerimeterShapeOperation();

// la figure
Rectangle rectangle = new Rectangle();
Double result = rectangle.accept( PerimeterOperation );

Ce qui se passe dans la méthode accept(…) :

// "operation" correspond à "perimeterOperation"
public Double accept( ShapeOperation operation ) {

    // Correspond à PerimeterShapeOperation.calcul(Rectangle)
    return operation.calcul( this );
}

La variable result contiendra le résultat, c’est-à-dire ici le périmètre du rectangle.

 

Cas défavorable où il ne faut pas utiliser le design pattern.

Cette solution convient parfaitement lorsque l’on ne fait qu’ajouter des responsabilités aux classes existantes (ici des opérations). Mais dans le cas où ce ne sont pas les opérations qui changent régulièrement mais les figures. Ce design pattern montre tout de suite ses limites. On est vite invité à modifier une multitude de fichiers comme dans la première situation.

Il est préférable en ces circonstances de l’éviter et choisir à la rigueur la première solution