Le visitor design pattern; unifier des objets différents

Morgan Asnar
4 min readDec 5, 2018

--

Le Visitor Design Pattern, ou VDP, est une manière d’appliquer un traitement sur les données d’objets issus d’une structure de données. Concrètement, on a un ensemble d’objets, ces objets, souvent différents, ont des caractéristiques, et on effectue, à l’aide du VDP, des opérations sur ces caractéristiques.

Au cœur de ce mécanisme, on distingue deux types de classes principales, qui portent chacune une méthode spécifique.

→ Tout d’abord, on a un objet visiteur, qui devra disposer d’une méthode visit(Objet). C’est elle qui définira les opérations à appliquer sur l’objet qui lui sera passé en paramètre. Dans le cas où l’on a plusieurs types d’objets différents que le même visiteur devra traiter, on peut définir plusieurs fonctions visit(), qui pourront effectuer des traitements propres à chaque objet.

→ Ensuite, on a l’objet sur lequel on va appliquer le VDP. Il doit disposer d’une méthode accept(Visiteur), qui va lancer la fonction visit() du visiteur passé en paramètre. Ce système permet au visiteur d’accéder aux méthodes, comme les getters, qui sont propres à l’objet.

Ainsi, le programme principal lancera un appel de la forme Objet.accept(Visiteur) pour déclencher l’opération. Il faut alors noter que cet appel de fonction peut renvoyer n’importe quoi: un entier, un objet, un String… Le type de renvoi n’est pas lié à la notion de VDP; il est propre à chaque visiteur. Il convient donc de faire attention, car les méthodes visit() et accept() se doivent de renvoyer le même type d’objet.

On peut alors se demander ce qu’apporte un tel procédé. Après tout, si on a besoin d’appliquer une opération définie, propre à chaque objet, pourquoi ne pas définir une méthode de la classe Objet qui l’effectuerait?

Tout d’abord, on peut de cette façon séparer très nettement les objets des calculs qui sont effectués dessus. En effet, même si les objets sont de types distincts, et que leurs données diffèrent, créer un visiteur pour une opération permet de récupérer une donnée similaire pour chaque objet.

Le code est ainsi simplifié, plus pratique à utiliser. Si on veut appliquer une opération sur un objet, on appelle le visiteur, sans même avoir à se soucier du type de l’objet. Par ailleurs, l’ajout d’une opération est également simplifié, puisqu’il suffit d’ajouter une instance de la classe visiteur, plutôt que de créer dans chaque classe une fonction qui appliquerait l’opération.

Le fragment de code qui suit, issu du site “geeksforgeeks” (cf sources en fin d’article), illustre très bien l’intérêt de ce mécanisme.

public static void main(String[] args)
{
ItemElement[] items = new ItemElement[]{new Book(20, “1234”),new Book(100, “5678”),
new Fruit(10, 2, “Banana”), new Fruit(5, 5, “Apple”)};

int total = calculatePrice(items);
System.out.println(“Total Cost = “+total);
}

interface ShoppingCartVisitor
{

int visit(Book book);
int visit(Fruit fruit);
}

class ShoppingCartVisitorImpl implements ShoppingCartVisitor
{

@Override
public int visit(Book book)
{
int cost=0;
//apply 5$ discount if book price is greater than 50
if(book.getPrice() > 50)
{
cost = book.getPrice()-5;
}
else
cost = book.getPrice();

System.out.println(“Book ISBN::”+book.getIsbnNumber() + “ cost =”+cost);
return cost;
}

@Override
public int visit(Fruit fruit)
{
int cost = fruit.getPricePerKg()*fruit.getWeight();
System.out.println(fruit.getName() + “ cost = “+cost);
return cost;
}

}

private static int calculatePrice(ItemElement[] items)
{
ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
int sum=0;
for(ItemElement item : items)
{
sum = sum + item.accept(visitor);
}
return sum;
}

On dispose d’un tableau d’objets, le panier de courses, dont les caractéristiques sont différentes. D’un coté, un prix au kg, puis la masse qu’on achète, et enfin le nom du fruit, de l’autre juste un prix et une côte. Cependant, on souhaite parcourir le tableau et leur appliquer un opérateur qui nous permettrait, pour chaque objet, de le présenter et d’en donner le prix. Les objets sont différents, et les opérations qui s’appliquent dessus le sont également, comme on peut le voir dans la définition du visiteur ci-dessous, pourtant on utilise le même appel de fonction pour tous les objets.

interface ShoppingCartVisitor
{

int visit(Book book);
int visit(Fruit fruit);
}

class ShoppingCartVisitorImpl implements ShoppingCartVisitor
{

@Override
public int visit(Book book)
{
int cost=0;
//apply 5$ discount if book price is greater than 50
if(book.getPrice() > 50)
{
cost = book.getPrice()-5;
}
else
cost = book.getPrice();

System.out.println(“Book ISBN::”+book.getIsbnNumber() + “ cost =”+cost);
return cost;
}

@Override
public int visit(Fruit fruit)
{
int cost = fruit.getPricePerKg()*fruit.getWeight();
System.out.println(fruit.getName() + “ cost = “+cost);
return cost;
}

}

Le code est ainsi significativement plus lisible.

Un autre exemple de l’usage de ce procédé peut être le rendu de scène 3D; on dispose de points, de vecteurs, qui peuvent être traités de façons différentes, selon le rendu que l’on souhaite obtenir.

Exemple de deux rendus différents de la même scène 3D

Ainsi, si l’on souhaite ajouter un nouveau type de rendu, comme en fil de fer par exemple, il s’agira simplement d’une nouvelle façon de traiter les données; il suffira donc de rajouter un visiteur dédié à cette tâche.

Si l’on devait résumer l’utilité de ce procédé en un mot, ce serait sûrement lisibilité. Le code, avec ce processus, gagne en propreté et en souplesse lorsque l’on travaille sur des bases de donnés hétéroclites. Cependant, il nécessite de connaitre la façon dont sont gérés les objets, et la possibilité de leur implémenter une fonction visit().

Sources:

--

--