next up previous contents index
Next: Les flots Up: Délégation Previous: L'exemple des threads

     
Un pattern de délégation : les visiteurs

On doit représenter les expressions arithmétiques et effectuer un certain nombre de traitements sur celles-ci, par exemple, les évaluer, les imprimer de façon infixe, ou suffixe, etc. Une expression est soit une constante, soit l'addition de deux expressions, soit la multiplication de deux expressions, etc. On transcrit cette définition en une hiérarchie de classes : la classe parente est une classe abstraite Expr, ses classes dérivées sont des classes concrètes Const, Plus, Mult, etc. Supposons ces types définis ; il sera naturel de définir, par exemple, l'objet suivant :

    Expr expr =                         // expr = 2 + (3+6)
      new Plus(new Const(2), 
               new Plus(new Const(3),
                        new Const(6)));

Les classes concrètes Const, Plus, etc., sont dérivées de Expr :

class Const extends Expr {
  private int c;
  Const(int c) {
    this.c = c;
  }
   ...
}

class Plus extends Expr {
  private Expr expr1, expr2;
  Plus(Expr expr1, Expr expr2) {
    this.expr1 = expr1;
    this.expr2 = expr2;
  }
   ...
}

Une façon de procéder serait de définir des méthodes d'évaluation (eval()) et d'affichage (infixe()) dans la classe Expr et ses classes dérivées.

    Expr expr = ...                      // expr = 2 + (3+6)
    Object valeur = expr.eval();         // valeur = 11
    Object chaine = expr.infixe();       // chaine = "2+(3+6)"

Une autre façon est de déléguer ces fonctions à un autre objet, appelé visiteur. On introduit donc une interface pour typer ces visiteurs d'expression, avec autant de méthodes que de classes concrètes d'expression :

interface ExprVisiteur {
  Object visiterConst(int c);
  Object visiterPlus(Expr expr1, Expr expr2);
}

Chacune de ces méthodes doit traiter une instance d'une classe d'expressions et produire un objet. Il y aura autant d'implémentations de cette interface que de traitements demandés :

class EvalVisiteur implements ExprVisiteur { ... }
class InfixeVisiteur implements ExprVisiteur { ... }

La classe Expr n'a plus besoin que d'une unique méthode pour associer un de ses objets à un traitement. On appelle cette méthode déléguer(), car l'expression << délègue >> le traitement à un visiteur, et on l'utilise ainsi :

    Expr expr = ...                          // expr = 2 + (3+6)
    ExprVisiteur eval = new EvalVisiteur();
    ExprVisiteur infixe = new InfixeVisiteur();
    Object valeur = expr.déléguer(eval);    // valeur = 11
    Object chaine = expr.déléguer(infixe);  // chaine = "2+(3+6)"

Ainsi, les données (les expressions) et les traitements (évaluations, etc.) sur ces données sont découplés en deux objets distincts. On doit donc définir une méthode abstraite dans la classe Expr :

abstract class Expr {
  abstract Object déléguer(ExprVisiteur v);
}

Il faut l'implémenter dans chaque classe dérivée, en appelant la méthode du visiteur spéciale à cette classe :

class Const extends Expr {
  private int c;
  Const(int c) {
    this.c = c;
  }
  Object déléguer(ExprVisiteur v) {
    return v.visiterConst(c);
  }
}

class Plus extends Expr {
  private Expr expr1, expr2;
  Plus(Expr expr1, Expr expr2) {
    this.expr1 = expr1;
    this.expr2 = expr2;
  }
  Object déléguer(ExprVisiteur v) {
    return v.visiterPlus(expr1, expr2);
  }
}

Il reste à implémenter les méthodes des visiteurs, pour chaque classe d'expression et pour chaque traitement demandé. Comme tous les traitements doivent retourner un Object, l'évaluation retourne un Integer (pas un int!) et l'impression retourne un String, qui sont des Objects :

class EvalVisiteur implements ExprVisiteur {
  public Object visiterConst(int c) {
    return new Integer(c);
  }
  public Object visiterPlus(Expr expr1, Expr expr2) {
    return 
      new Integer(((Integer)expr1.déléguer(this)).intValue() +
                  ((Integer)expr2.déléguer(this)).intValue());
  }
}

class InfixeVisiteur implements ExprVisiteur {
  public Object visiterConst(int c) {
    return Integer.toString(c);
  }
  public Object visiterPlus(Expr expr1, Expr expr2) {
    return 
      "(" +  
      expr1.déléguer(this) + 
      "+" + 
      expr2.déléguer(this) +
      ")";
  }
}

Les données et les traitements étant découplés, si un nouveau traitement doit être programmé, il suffit d'écrire une nouvelle classe dérivée de ExprVisiteur, sans toucher aux autres ni toucher aux différentes classes d'expressions. Si par contre, on ajoute une nouvelle classe d'expressions (les produits, divisions, etc.), il faut aussi ajouter une méthode pour cette classe dans chacune des classes concrètes de visiteurs.

Java ne permet pas, à la différence de certains langages (C, C++, et bien sûr les langages fonctionnels comme Ocaml) de passer en argument une fonction. Ceci s'avère pourtant utile : passer en argument une fonction de comparaison à une fonction de tri, ou une fonction de traitement des éléments à une fonction de parcours d'une structure de données. L'usage des interfaces permet d'y suppléer, comme nous l'avons vu avec l'interface Comparator, et de façon plus souple, avec la notion de visiteur.


next up previous contents index
Next: Les flots Up: Délégation Previous: L'exemple des threads
R. Lalement
2000-10-23