Chain of responsability

La programmation avec design patterns

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Souvent le traitement d'un élément ne passe pas par une seule personne. Prenons le cas d'un jury d'admission à une formation. Tous les membres doivent alors se prononcer en ce qui concerne les aptitudes d'élève à rejoindre une formation. Dans le développement le même traitement à la chaîne est présente dans chain of responsability.

Le design pattern chain of responsability (chaîne de responsabilité) est une bonne illustration informatique du traitement à la chaîne. Son fonctionnement repose sur la décomposition d'une opération en plusieurs tâches. Chaque de ces tâches est effectuée par un objet séparé. Le résultat final correspond à la réalisation de toutes les tâches.

Un des gros avantages de la chaîne de responsabilité est la flexibilité. On peut décomposer une opération en nombre de tâches indéfinie. Cela peut être utile dans des opérations plus complexes, comme par exemple analyse des images avec plusieurs filtres graphiques. On peut alors imaginer que la première tâche de traitement consiste éliminer le bruit d'une image (lissage). Ensuite, sur une image plus pure, on pourra introduire la transparence à l'image et ainsi de suite. Le nombre d'opération est donc infinie.

La seule limite d'exécution non fixée explicitement dans le code, peut être une erreur d'exécution. Imaginons que notre système de filtrage lance une exception non captée au deuxième filtre sur 10. Alors l'exécution d'autres filtres serait comprise. Il faudra prévoir cette possibilité de plantage dans le code, afin de garder le résultat final très près de celui attendu (même en cas d'échec d'un maillon de la chaîne).

Grâce à cette gestion d'exceptions réfléchie, on peut rajouter des opérations à la chaîne sans se préoccuper de la stabilité de la chaîne. Or même en rajoutant un objet potentiellement défaillant, on est sûr qu'il ne compromettra pas l'opération générale. Cette idée de séparation garantit également un meilleur niveau d'isolation. Les modifications d'un objet n'impacteront pas le comportement d'un autre objet. Les deux sont pourtant indépendants.

La chaîne de responsabilité est composée de 3 acteurs :
- handler (gestionnaire) : définition de l'interface qui va gérer la requête à la chaîne.
- concrete handler (gestionnaire concret) : l'implémentation concrète de l'handler. Il peut traiter la requête d'une manière différente ou alors se contenter de la transmettre à l'objet suivant dans la chaîne.
- client : l'objet qui va initialiser le traitement.

Pour voir plus précisément en quoi consiste ce patron de conception, on prendra l'exemple de création d'une pizza au thon. Regardons ceci plus bas.

Exemple du chain of responsability

abstract class Pizza{
protected Pizza ingredient;

public abstract boolean isForThonPizza();

public void addIngredient(){
if(isForThonPizza()){
System.out.println("Adding : "+this);
}
if(this.ingredient!=null){
this.ingredient.addIngredient();
}
}

public void setNext(final Pizza ingredient){
this.ingredient=ingredient;
}
}

Ce code définit notre gestionnaire. On la présence d'une méthode abstraite (isForThonPizza()). Elle déterminera si un ingrédient fera partie d'une pizza au thon. Si ce sera le cas, on le rajouterai à la composition. Sinon, on passera directement à la méthode qui invoquera automatiquement le maillon suivant de la chaîne de responsabilité.


class Salt extends Pizza{
@Override
public boolean isForThonPizza(){
return false;
}

@Override
public String toString(){
return "Salt";
}
}

class Tuna extends Pizza{
@Override
public boolean isForThonPizza(){
return true;
}

@Override
public String toString(){
return "Tuna";
}
}

class Cheese extends Pizza{
@Override
public boolean isForThonPizza(){
return true;
}

@Override
public String toString(){
return "Cheese";
}
}

class Olive extends Pizza{
@Override
public boolean isForThonPizza(){
return true;
}

@Override
public String toString(){
return "Olive";
}
}

Ci-dessus les implémentations concrètes du gestionnaires. On remarques qu'elles ont toutes des méthodes isForThonPizza() et toString() définies.


public static void main(final String[] args){
final Pizza cheese=new Cheese();
final Pizza salt=new Salt();
cheese.setNext(salt);
final Pizza tuna=new Tuna();
salt.setNext(tuna);
final Pizza olive=new Olive();
tuna.setNext(olive);
cheese.addIngredient();
}

C'est le code que pourra utiliser le client pour créer la pizza. On y mis tous les ingrédients disponibles au stock (dont ceux qui ne sont pas admis dans une pizza au thon). On observe que tous les ingrédients sauf dernier contiennent une méthode setNext(), dans laquelle on passe l'ingrédient suivant à rajouter.


Adding : Cheese
Adding : Tuna
Adding : Olive

Ici on voit le résultat de traitement. On voit bien que le sel, malgré sa présence dans le code du client, n'est pas rajouté. C'est parce qu'il retourne false dans la méthode isForThonPizza(). Tous les ingrédients sont invoqués grâce à l'appel this.ingredient.addIngredient() à l'intérieur de la méthode addIngredient() de la classe abstraite Pizza.

La chaîne de responsabilité est alors une bonne solution aux traitements des données une derrière l'autre. Sa bonne implémentation garantit également que toutes les étapes, indépendamment de leur résultat final, seront invoquées. Il faut néanmoins rester vigilant et prévoir même le scénario le plus catastrophique.
Bartosz KONIECZNY 06-10-2013 16:14 design patterns
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil Symfony1

Comment changer le contexte ?

Si vous travaillez sur un module du backend et avez besoin d'utiliser pendant un moment l'élément d'une autre application (par exemple, frontend), il suffit de changer le contexte par :

sfContext::switchTo('frontend');