Design Pattern Command

Design Pattern Command

Description

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

Préambule

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 oeil à cette source.

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

Ce document intitulé « Design Pattern Command » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous