Vous ne trouvez pas de réponse à votre problème ? Alors posez la question dans le forum. Souvenez-vous qu'il n'y a jamais de question bête, mais rester dans l'ignorance parce que l'on n'ose pas poser une question, ça c'est une erreur !

COMMAND PATTERN


Information sur le tutorial

Catégorie :Tutoriaux Tutorial .NET ( DotNet ) Date de création : 27/02/2008 17:19:32 Vu : 6 677 fois

Note :
Aucune note

Commentaire sur cette source (4)
Ajouter un commentaire et/ou une note


Description

Design Pattern Command C#
Présentation du pattern Command avec un petit exemple simple à l'appui pour illustrer le concept...

Tutorial

Command Pattern

Autre design pattern

Observer
Strategy

Introduction

Le but du Command pattern est de pouvoir mettre en place un système de undo/redo pour pouvoir revenir en arrière d’un certains nombres d’actions ou au contraire, pour avancer d’un certains nombres d’actions (utilisation typique et indispensable dans Word, par exemple).

Le schéma, toujours en provenance du site dofactory (www.dofactory.com) nous montre ici la structure de base du Pattern :

Afin d’éviter des pages et des pages d’explications, je propose directement de passer à une implémentation pour voir de quoi il s’agit…

Implémentation

Reprenons le cas du robot que j’ai utilisé dans le cadre du Strategy pattern. L’idéal serait, quand on fait bouger ou sauter le robot, qu’il puisse revenir en arrière. Autrement dit, donner une possibilité à l’utilisateur d’annuler une ou plusieurs actions qu’il a donner à son robot.

On commence par créer une interface qui définit les actions que nos commandes feront, à savoir le do, undo, et une property qui va nous indiquer si une certaine action est « undoable »

/// ----------------------------------------------------------
/// <summary>
/// Command pattern.
/// </summary>
/// ----------------------------------------------------------
public interface ICommand
{
void Do();

void Undo();

/// ----------------------------------------------------------
/// <summary>
/// Return true if the current action is undoable.
/// </summary>
/// ----------------------------------------------------------
bool IsUndoable { get; }
}

A présent, il est possible de passer par une classe abstraite pour étendre le modèle au maximum, mais ce passage n’est pas obligatoire. Voici une implémentation de base qui peut (doit ?) être amélioré

public abstract class Command : ICommand
{
public abstract void Do();

public abstract void Undo();

/// ----------------------------------------------------------
/// <summary>
/// Undoable by default.
/// </summary>
/// ----------------------------------------------------------
public virtual bool IsUndoable
{
get { return true; }
}
}

Maintenant, on peut commencer à coder nos commandes, à savoir une commande pour faire avancer le robot, et une commande pour le faire sauter.

/// ----------------------------------------------------------
/// <summary>
/// Jump command.
/// </summary>
/// ----------------------------------------------------------
public class JumpCommand : Command
{
private IRobot _robot = null;
private PointF _currentLocation = PointF.Empty;
private PointF _oldLocation = PointF.Empty;

public JumpCommand(IRobot robot, PointF oldLocation, PointF currentLocation)
{
if (robot == null) throw new ArgumentException("Robot cannot be null");
this._robot = robot;
this._oldLocation = oldLocation;
this._currentLocation = currentLocation;
}

public override void Do()
{
this._robot.Jump(this._currentLocation);
}

public override void Undo()
{
this._robot.Jump(this._oldLocation);
}
}

/// ----------------------------------------------------------
/// <summary>
/// Move command.
/// </summary>
/// ----------------------------------------------------------
public class MoveCommand : Command
{
private IRobot _robot = null;
private float _dist = 0f;
private float _angle = 0f;

public MoveCommand(IRobot robot, float dist, float angle)
{
if (robot == null) throw new ArgumentException("Robot cannot be null");
this._robot = robot;
this._dist = dist;
this._angle = angle;
}

public override void Do()
{
this._robot.Move(this._dist, this._angle);
}

public override void Undo()
{
this._robot.Move(-this._dist, this._angle);
}

/// ---------------------------------------------------------------
/// <summary>
/// This action is undoable only if the robot move more than 100.
/// </summary>
/// ---------------------------------------------------------------
public override bool IsUndoable
{
get { return this._dist > 100; }
}
}


Voilà, il ne reste plus qu’à faire une classe qui va pouvoir conserver une liste de commande « do » et une liste de commande « undo ». Ca donne quelques chose comme ceci :

/// ----------------------------------------------------------
/// <summary>
/// Group of command.
/// </summary>
/// <typeparam name="T">The type of the collection.</typeparam>
/// ----------------------------------------------------------
public class CommandGroup<T> where T : ICommand
{
private Stack<T> _commandsDo = new Stack<T>();
private Stack<T> _commandsUndo = new Stack<T>();

public void Add(T item)
{
this._commandsDo.Push(item);
}

public void Remove(T item)
{
throw new NotImplementedException();
}

public bool CanDo
{
get { return this._commandsUndo.Count > 0; }
}

public bool CanUndo
{
get { return this._commandsDo.Count > 0 && this._commandsDo.Peek().IsUndoable; }
}

public void ClearAll()
{
this._commandsDo.Clear();
this._commandsUndo.Clear();
}

public void Undo(int count)
{
if (this._commandsDo.Count >= count)
{
for (int i = 0; i < count; i++)
{
var command = this._commandsDo.Peek();
if (command.IsUndoable)
{
this._commandsDo.Pop();
command.Undo();
this._commandsUndo.Push(command);
}
}
}
else throw new InvalidOperationException("Not enougth actions to undo");
}

public void Do(int count)
{
if (this._commandsUndo.Count >= count)
{
for (int i = 0; i < count; i++)
{
var command = this._commandsUndo.Peek();
if (command.IsUndoable)
{
this._commandsUndo.Pop();
command.Do();
this._commandsDo.Push(command);
}
}
}
else throw new InvalidOperationException("Not enougth actions to do");
}
}

Les classes sont prêtes, il ne reste plus qu’à les exploiter…

/// ----------------------------------------------------------
/// <summary>
/// Move the robot.
/// </summary>
/// <param name="dist">The amount to move.</param>
/// <param name="angle">The amount to rotate.</param>
/// <param name="save">True to save this command.</param>
/// ----------------------------------------------------------
public float Move(float dist, float angle, bool save)
{

if (save)
{
var mc = new MoveCommand(this, effDist, angle);
this._cGroup.Add(mc);
}
return …;
}

/// ----------------------------------------------------------
/// <summary>
/// Jump.
/// </summary>
/// <param name="newLocation">The old location.</param>
/// <param name="save">True to save this command.</param>
/// ----------------------------------------------------------
public void Jump(PointF newLocation, bool save)
{

if (save)
{
var mc = new JumpCommand(this, this._location, newLocation);
this._cGroup.Add(mc);
}
}

Conclusion

Un pattern qui n’est pas si simple qui n’y paraît et qui peut même devenir assez complexe si on veut gérer proprement le undo (par exemple, que se passe t’il si on fait 2 undo puis on continue d’utiliser le programme (à faire des actions) et après un certain laps de temps on fait un redo ?).
Très utile dans un logiciel de traitement (texte, image, etc) il est cependant pas autant utiliser qu’un Strategy, Observer ou même Factory pattern.

Pour une implémentation dans un programme de ce pattern, je vous conseille de jeter un œil à cette source

Pour plus de lecture à ce sujet, vous pouvez consulter un tutorial de yoannd qui se trouve ici

27 février 2008 20:33:36 :
Mise en page
27 février 2008 20:40:01 :
Re mise en page
27 février 2008 20:47:26 :
Encore mise en page...
27 février 2008 20:58:09 :
Encore....
27 février 2008 23:21:50 :
Ajout lien sur source
signaler à un administrateur
Commentaire de yoannd le 28/02/2008 10:26:23

Salut Bidou,

J'ai regardé ce pattern très intéressant, et j'ai juste une ou deux petites remarques :
1. N'y aurait-il pas une inversion entre les objets _commandsDo et _commandsUndo ? Ainsi, dans ta méthode Add, tu devrais ajouter ta commande dans la liste des undo :
public void Add(T item)
{
this._commandsUndo.Push(item);
}

ce qui fait qu'il y aurait aussi un problème dans les méthodes do et undo.

2. Au niveau du comportement de ta solution par rapport au comportement des undo/redo dans word.
Dans Word, si tu tapes du texte, et que tu fais Undo, le bouton Redo se dégrise. Par contre, si tu re-tapes du texte, l'historique des commandes côté "Redo" est effacée. En conclusion, je pense que le code de la méthode Add doit être le suivant :

public void Add(T item)
{
    this._commandsUndo.Push(item);
    this._commandsDo.Clear();
}

Pour info, j'avais posté aussi un tuto sur le sujet : http://www.csharpfr.com/tutorial.aspx?id=818

signaler à un administrateur
Commentaire de Bidou le 28/02/2008 12:03:03 administrateur CS

Salut et merci du commentaire.

Pour le point 1), ce n'est pas une erreur. Quand on ajoute une commande, on la met dans le "do". Lors d'un undo, on lit ce qui se trouve dans le do et on place les éléments dans le undo et inversément pour le "undo".

Pour le point deux, c'est effectivement possible que j'aie fait une erreur, je regarderai au plus vite pour corriger (peut-être ce soir si j'ai le temps).

signaler à un administrateur
Commentaire de yoannd le 28/02/2008 12:55:18

Oui d'accord, je vois : On a pas le même principe dans nos 2 algorithmes respectifs, et personne n'a raison, personne n'a tors ^^
Je m'explique :
Lorsque tu ajoutes une commande, tu la mets dans les Do parce que c'est une opération qui ont a effectuée.
Je prend la logique inverse : lorsque j'ajoute une commande, je la met dans les Undo car elle fait maintenant partie de la liste des actions que je peux annuler.

Bref, tout n'est ici qu'une question de "vocabulaire" et les 2 sont valables.

Pour ma part, je pense qu'il est tout à fait indiqué d'agrémenter ce code avec plusieurs éléments :
- Faire de CommandGroup un singleton pour pouvoir y accéder depuis les divers objets métiers, et depuis l'interface,
- Ajouter des évènements sur cet objet qui se déclencheraient lorsque le CanUndo ou le CanRedo changent.
Partant de ce principe, je fais en sorte que mes objets métiers mettent à jour le singleton. Exemple : tu as la classe "Personne", elle met à jour le CommandGroup dans le "set" sur la propriété "Nom".
Partant de là, le statut de la propriété CanUndo change -> le singleton envoie un évènement.
Cet évènement est récupéré par un user control qui représente le bouton "Annuler" de mon application, histoire qu'il se dégrise. Au clic sur ce bouton, j'appelle simplement la méthode undo de mon simpleton.
Avec ça, on a vraiment un principe d'autonomie des classes.

Enfin pour ce tuto, je pense pas qu'il faille mettre tous les enchainements que je viens de décrire : on s'attache bien au design patern en lui-même, et pas au reste... donc c'est pas utile. C'est pour ça que je mets une bonne note : on cible bien le problème de façon claire ici (très certainement de façon bien plus claire que je ne l'ai fait).

Bon coding à toi ^^

signaler à un administrateur
Commentaire de objinfo le 11/03/2008 17:04:24

Pour Yoannd,
Architecte depuis plus de 15 ans, je connais bien ce pattern que j'ai implémenté ou fait implémenter à plusieurs reprises.
Je dois te signaler que ton approche consistant à mettre à jour le CommandGroup à partir de l'objet metier est une erreur de conception relativement commune, et assez grave.
Les classes gérant les commandes appartiennent à la couche "service", elles ne doivent donc pas être accédées par des classes de la couche "domaine" (seul l'inverse doit pouvoir se produire).
Si tu devais, par exemple, réutilser tes objets métier dans un autre contexte (une appli ASP par exemple), tu serais ainsi obligé de te traîner la dépendance sur CommandGroup qui ne serait peut-être plus pertinente dans ce contexte.
Par ailleurs, ce faisant, tu passes à côté du principal intérêt de ce pattern, dont la bonne pratique consiste à calquer la granularité des commandes sur celle des cas d'utilisation: c'est le seul moyen de maîtriser le comportement d'un undo dans une appli complexe
Prenons un exemple :
> Ton application a deux cas d'utilisation :
  "Mettre à jour le nom de la personne"
  "Marier la personne"
> On implémente les deux cas d'utilisation par deux commandes, la seconde appelant la première si la personne est une femme
> Lorsque la commande "Marier.." est appelée, seule cette commande est ajoutée dans le CommandGroup. Si le set sur Nom ajoute une commande (et pourquoi pas le set sur adresse, sur Prenom), plusieurs commandes seront ajoutées, et le Undo sur "Marier.." n'annulera pas la globalité de cette action, mais uniquement sa partie spécifique.
Concrètement, l'utilisateur appuyant sur Ctrl-Z annulera son mariage sans récupérer son nom de jeune fille, un nouvel appui sur Ctrl-Z étant nécessaire pour ça.
Autre utilité du pattern command aligné sur les cas d'utilisation : la possibilité de piloter une appli par scripts, ce qui peut être d'une grande aide pour des tests métier et/ou des tests de non régression.

Ajouter un commentaire



Nos sponsors

Sondage...

CalendriCode

Juillet 2009
LMMJVSD
  12345
6789101112
13141516171819
20212223242526
2728293031  

Consulter la suite du CalendriCode

Comparez les prix Nouvelle version

Photothèque Nouveau !



Développement réalisé par Nicolas SOREL (Nix) avec l'aide de : Cyril DURAND et Emmanuel (EBArtSoft), Merci à Vincent pour ses précieux conseils
CodeS-SourceS.com© Toute reproduction même partielle est interdite sauf accord écrit du Webmaster
CodeS-SourceS.com© est une marque déposée tous droits réservés
Temps d'éxécution de la page : 0,078 sec

Google Coop CodeS-SourceS Google Coop CodeS-SourceS


Certaines images présentes sur le site (notament certains avatars) sont issues des collections IconShock, donc si vous souhaitez utiliser ces icons vous devez les acheter, ne les copiez pas et ne utilisez pas dans vos sites et applications sans les avoir commandé.