// you’re reading...

tutorial

Design Patterns #4 – Command Pattern

Aujourd’hui nous allons aborder le command pattern.

Ce modèle de conception est assez intuitif. Il s’agit d’encapsuler à l’intérieur d’un objet (plus communément appellé commande) le déroulement d’une action. A chaque fois que je voudrais exécuter cette action à l’intérieur d’une application, il me suffira d’appeller la méthode execute() de mon objet commande.

Commençons par un exemple basique :

class RebootCommand
{
 	public function execute() : Void
 	{
  		trace("reboot my PC");
  	}
}
import RebootCommand;

var oReboot:RebootCommand = new RebootCommand();
oReboot.execute();

La classe RebootCommand une fois instanciée me permet de rebooter mon ordinateur à chaque appel de sa méthode execute.

A partir de ce postulat, il devient évident que pour pouvoir être exécutée, une commande doit obligatoirement implémenter une méthode execute. Cette méthode contiendra les éléments nécéssaires au bon déroulement de l’action sollicitée.

Par conséquent, une commande devra obligatoirement implémenter l’interface suivante :

interface Command
{
 	public function execute() : Void;
}

Reprenons maintenant le même exemple en utilisant l’interface ci-dessus :

import Command;

class RebootCommand implements Command
{
 	public function execute() : Void
 	{
  		trace("reboot my PC");
  	}
}

L’utilisation de l’interface Command : – Garantit que ma commande possède bien une méthode execute pour être déployée. – Permet (comme nous le verrons plus tard) de recourir au polymorphisme pour exécuter des commandes diverses sans se soucier de leur implémentation concrète.

Point essentiel et souvent controversé, la méthode execute ne renvoit rien mais SURTOUT n’autorise aucun paramètre. En effet, une commande, au moment de son exécution est censée déjà détenir les éléments nécessaires pour pouvoir interagir avec le système.

De prime abord, cela peut paraître un peu rigide et vous devez vous demander comment procéder pour spécifier des interactions avec le coeur de l’application ?

Il suffit tout simplement d’utiliser le constructeur de votre classe concrète pour pouvoir stocker les propriétés qui seront nécessaires au moment de l’exécution.

Prenons un deuxième exemple pour illustrer ceci :

import Command;

class DisplayDateCommand implements Command
{
 	private var _tViewHelper:TextField;

 	public function DisplayDateCommand(tViewHelper:TextField)
 	{
  		_tViewHelper = tViewHelper;
  	}

 	public function execute() : Void
 	{
  		_tViewHelper.text = String(new Date());
  	}
}	}
}

Dans l’exemple ci-dessus on utilise un helper en paramètre dans le constructeur pour spécifier la cible graphique qui affichera la date du jour.

Il est biensûr possible d’ajouter des accesseurs dans votre classe concrète pour pouvoir modifier à tout moment les propriétés requises pour l’exécution de votre commande :

import Command;

class DisplayDateCommand implements Command
{
 	private var _tViewHelper:TextField;

 	public function DisplayDateCommand(tViewHelper:TextField)
 	{
  		_tViewHelper = tViewHelper;
  	}

 	public function execute() : Void
 	{
  		_tViewHelper.text = String(new Date());
  	}

 	public function setViewHelper(tViewHelper:TextField) : Void
 	{
  		_tViewHelper = tViewHelper;
  	}
}
import DisplayDateCommand;

this.createTextField("__txt", 1, 30, 30, 200, 20);
var ddc:DisplayDateCommand = new DisplayDateCommand();
ddc.setViewHelper(this.__txt);
ddc.execute();

Maintenant, passons à la question phare, celle que vous êtes tous en droit de vous poser : Quel est l’intérêt de ce modèle de conception ?

L’intérêt majeur, c’est de pouvoir découpler l’objet qui crée une requête pour une action donnée et l’action elle-même ou le receveur de celle-ci. L’exemple récurrent pour illustrer ce concept, c’est l’utilisation d’UIs (interfaces utilisateur) à l’interieur d’une application. Une interface utilisateur est censée ignorer l’implémentation et le déroulement des requêtes qu’elle formule. Autrement dit, le code des requêtes doit donc en aucun cas être exposé à celle-ci. On préférera l’encapsuler à l’intérieur de commandes, classes concrètes implémentant la classe abstraite Command (interface Command en AS2.0). Les UIs n’auront plus qu’à invoquer les commandes disponibles pour obtenir satisfaction à leurs requêtes.

import DisplayDateCommand;
import mx.controls.Button;

this.createTextField("__txt", 2, 30, 30, 200, 20);
var tDisplay:TextField = this.__txt;

var oDdC:DisplayDateCommand = new DisplayDateCommand();
oDdC.setViewHelper(tDisplay);

var pbGetDate:Button = this.createClassObject(Button, "__pbGetDate", 1);
pbGetDate.move(30, 60);
pbGetDate.label = "Display";
pbGetDate.addEventListener("click", this);

function click() : Void
{
 	oDdC.execute();
}

Deuxième aspect non négligeable, c’est la possibilité d’atomiser les fonctionnalités d’une application en objets indépendants pour ne pas surcharger le contrôleur d’une application. On constatera par expérience que niveau design, il est toujours plus facile et élégant de changer par exemple l’implémentation d’une commande SeConnecter à l’intérieur d’une classe dédiée que de parcourir 2.000 lignes de code d’un contrôleur pour modifier celle-ci.

L’atomisation des fonctionnalités peut permettre aussi de créer des classes composites pour définir des macro-commandes (enchaînement de plusieurs commandes).

import Command;

interface MacroCommand extends Command
{
 	public function addCommand(oCommand:Command) : Void;
}
import MacroCommand;

class MyMacroCommand implements MacroCommand
{
 	private var _aCommands:Array;

 	public function MyMacroCommand()
 	{
  		_aCommands = new Array();
  	}

 	public function addCommand(oCommand:Command) : Void
 	{
  		_aCommands.push(oCommand);
  	}

 	public function execute() : Void
 	{
  		var l:Number = _aCommands.length;
  		for (var i:Number = 0; i<l; i++) _aCommands[i].execute();
  	}
}
import MyMacroCommand;
import RebootCommand;
import DisplayDateCommand;

this.createTextField("__txt", 2, 30, 30, 200, 20);

var oMMC:MyMacroCommand = new MyMacroCommand();
oMMC.addCommand( new RebootCommand() );
oMMC.addCommand( new DisplayDateCommand(this.__txt) );

oMMC.execute();

addCommand utilise le polymorphisme d’interface que j’évoquais au début de cet article. Cette méthode attend en paramètre un objet de la famille Command, la nature exacte de celui-ci lui importe peu.

Le dernier aspect est le plus intéressant à mon goût. Il s’agit de déployer le command pattern à l’intérieur d’un front controller pour gagner en souplesse lors du développement et la maintenance d’une application.

Ceci sera le sujet du prochain volet. ;)

Voici pour finir une fiche technique résumant les caractéristiques du modèle Command :

Nom : Command pattern
Le problème :
Découpler les objets qui invoquent une action de ceux qui l’exécutent.
La solution :
Créer une classe abstraite Command (interface) avec une méthode execute. Chaque commande (action) d’une application sera une classe concrète implémentant la méthode virtuelle execute. Les objets qui en feront la requête n’auront plus qu’à trigger celle-ci sans en connaître l’implémentation
Les conséquences :
- Le command pattern éradique tout couplage potentiel entre l’objet qui invoque une opération et celui qui sait comment la réaliser.
- Une commande est un objet à part entière. Elle peut être manipulée, voire même étendue comme bon vous semble.
- On peut créer des commandes composites (cf: MacroCommand).
- Les commandes peuvent offrir la possibilité de faire et défaire (undo et niveaux d’undos).
- Les commandes jugulées à un front-controller offrent à une application une souplesse et une capacité de maintenance indéniables.

Les fichiers sources sont disponibles ici.

Voilà, cet article touche à sa fin. J’ai tenté plus ou moins adroitement de vulgariser ce modèle de conception et de l’illustrer avec des exemples qui restent le plus simple possible afin que tout le monde puisse y trouve son compte. La suite bientôt, avec au programme quelques notions un peu plus avancées cette fois-ci : Création d’un Front Controller déployant le Command pattern avec ViewHelper à l’appui.

Il est possible qu’entre temps je vous fasse d’abord faire un petit détour par le modèle Observer et vous propose une manière qui me semple plus adéquate pour gérer les événements en AS 2.0 que celle proposée par EventDispatcher. (Avis 100% subjectif bien entendu) En effet, la force d‘EventDispatcher c’est sa souplesse, mais c’est malheureusement aussi son talon d’achille. Je déplore son typage faible, son incapacité à scinder les écouteurs en plusieurs groupes (statique oblige) et l’impossibilité de savoir si pour un événement donné un ou plusieurs objets sont abonnés. Ce sera l’occasion pour moi de vous faire découvrir une autre façon de travailler avec les événements (plus rigide) et vous faire partager le package com.bourre.events de pixLib en avant-première.

En attendant n’hésitez pas à me faire partager vos éventuelles questions ou commentaires.

Discussion

No comments yet.

Post a comment