Patrons de conception/Commande


Commande est un patron de conception de type comportemental qui encapsule la notion d'invocation. Il permet de séparer complètement le code initiateur de l'action, du code de l'action elle-même. Ce patron de conception est souvent utilisé dans les interfaces graphiques où, par exemple, un item de menu peut être connecté à différentes Commandes de façons à ce que l'objet d'item de menu n'ait pas besoin de connaître les détails de l'action effectuée par la Commande.

Patron de conception
Catégorie : « Gang of Four »Comportement
Nom français : Commande
Nom anglais : Command
Encapsuler l'invocation d'une commande

À utiliser lorsque : il y a prolifération de méthodes similaires, et que le code de l'interface devient difficile à maintenir.

Symptômes:

  • Les objets possèdent trop de méthodes publiques à l'usage d'autres objets.
  • L'interface est inexploitable et on la modifie tout le temps.
  • Les noms des méthodes deviennent de longues périphrases.

Un objet Commande sert à communiquer une action à effectuer, ainsi que les arguments requis. L'objet est envoyé à une seule méthode dans une classe, qui traite les Commandes du type requis. L'objet est libre d'implémenter le traitement de la Commande par un switch, ou un appel à d'autres méthodes (notamment des méthodes surchargées dans les sous-classes). Cela permet d'apporter des modifications aux Commandes définies simplement dans la définition de la Commande, et non dans chaque classe qui utilise la Commande.

Diagramme de classes

modifier

Le patron de conception Commande peut être représenté par le diagramme de classes UML suivant :

 
Diagramme de classes UML du patron de conception Commande

Utilisations

modifier

Ce patron de conception peut être utilisé pour implémenter divers comportements :

Défaire sur plusieurs niveaux
Les actions de l'utilisateur sont enregistrées par empilement de commandes. Pour les défaire, il suffit de dépiler les dernières commandes et d'appeler leur méthode undo() pour annuler chaque commande.
Comportement transactionnel
La méthode d'annulation est appelée rollback() et permet de revenir en arrière si quelque chose se passe mal au cours d'une transaction (un ensemble de commandes). Exemples : installateurs de programmes, modification de base de données.
Barre de progression
Si chaque Commande possède une méthode d'estimation de durée, il est possible de représenter la progression de l'exécution d'un ensemble de tâches (Commandes).
Menu et boutons (interface graphique)
En Swing et Delphi, un objet Action est une Commande à laquelle on peut associer un raccourci clavier, une icône, un texte d'info-bulle ...
Wizards
Pour implémenter les boîtes de dialogue de type Wizard, une instance de Commande est créée. Chaque fois que l'utilisateur passe à la page suivante avec le bouton "Suivant" ("Next" en anglais), les valeurs entrées sont enregistrées dans la Commande. Le bouton "Terminer" ("Finish" en anglais) provoque l'exécution de la Commande.
Ensemble de threads (ThreadPool en anglais)
Un ensemble de threads exécute des tâches (Commandes) stockées dans une file.
Enregistrement de macros
Chaque action de l'utilisateur peut être enregistrée sous la forme d'une séquence de Commande qui peut être rejouée par la suite. Pour enregistrer les macros sous la forme de scripts, chaque commande possède une méthode toScript() pour générer le script correspondant.

Exemple

modifier
using System;
using System.Collections.Generic;

namespace CommandPattern
{
    public interface ICommand
    {
        void Execute();
    }

    /* The Invoker class */
    public class Switch
    {
        private List<ICommand> _commands = new List<ICommand>();

        public void StoreAndExecute(ICommand command)
        {
            _commands.Add(command);
            command.Execute();
        }
    }

    /* The Receiver class */
    public class Light
    {
        public void TurnOn()
        {
            Console.WriteLine("The light is on");
        }

        public void TurnOff()
        {
            Console.WriteLine("The light is off");
        }
    }

    /* The Command for turning on the light - ConcreteCommand #1 */
    public class FlipUpCommand : ICommand
    {
        private Light _light;

        public FlipUpCommand(Light light)
        {
            _light = light;
        }

        public void Execute()
        {
            _light.TurnOn();
        }
    }

    /* The Command for turning off the light - ConcreteCommand #2 */
    public class FlipDownCommand : ICommand
    {
        private Light _light;

        public FlipDownCommand(Light light)
        {
            _light = light;
        }

        public void Execute()
        {
            _light.TurnOff();
        }
    }

    /* The test class or client */
    internal class Program
    {
        public static void Main(string[] args)
        {
            Light lamp = new Light();
            ICommand switchUp = new FlipUpCommand(lamp);
            ICommand switchDown = new FlipDownCommand(lamp);

            Switch s = new Switch();
            string arg = args.Length > 0 ? args[0].ToUpper() : null;
            switch(arg)
            {
                case "ON":
                    s.StoreAndExecute(switchUp);
                    break;
                case "OFF":
                    s.StoreAndExecute(switchDown);
                    break;
                default:
                    Console.WriteLine("Argument \"ON\" or \"OFF\" is required.");
                    break;
            }
        }
    }
}

Considérons un interrupteur simple (switch en anglais). Dans cet exemple, on configure le switch avec deux commandes : une pour allumer la lumière, et une pour l'éteindre.

L'avantage de cette implémentation particulière du patron Commande est que l'interrupteur peut être utilisé avec n'importe quel périphérique, pas seulement une lampe. Dans l'exemple suivant l'interrupteur allume et éteint une lampe, mais le constructeur accepte toute classe dérivée de Command comme double paramètre. On peut, par exemple, configurer le switch pour démarrer et arrêter un moteur.

/* Invocateur */
public class Switch
{
    private Command flipUpCommand;
    private Command flipDownCommand;

    public Switch(Command flipUpCmd,Command flipDownCmd)
    {
        this.flipUpCommand=flipUpCmd;
        this.flipDownCommand=flipDownCmd;
    }

    public void flipUp()
    {
        flipUpCommand.execute();
    }

    public void flipDown()
    {
        flipDownCommand.execute();
    }
}

/* Récepteur */
public class Light
{
    public Light() {  }

    public void turnOn()
    {
        System.out.println("The light is on");
    }

    public void turnOff()
    {
        System.out.println("The light is off");
    }
}

/* Commande */
public interface Command
{
    void execute();
}


/* Commande concrète pour allumer la lumière */
public class TurnOnCommand implements Command
{
    private Light theLight;

    public TurnOnCommand(Light light)
    {
        this.theLight=light;
    }

    public void execute()
    {
        theLight.turnOn();
    }
}

/* Commande concrète pour éteindre la lumière */
public class TurnOffCommand implements Command
{
    private Light theLight;

    public TurnOffCommand(Light light)
    {
        this.theLight=light;
    }

    public void execute()
    {
        theLight.turnOff();
    }
}

/* Classe de test */
public class TestCommand
{
    public static void main(String[] args)
    {
        Light lamp = new Light();
        Command switchUp=new TurnOnCommand(lamp );
        Command switchDown=new TurnOffCommand(lamp );

        Switch s=new Switch(switchUp,switchDown);

        s.flipUp();
        s.flipDown();
    }
}
  # exemple de style "switch":
 
  sub doCommand {
    my $me = shift;
    my $cmd = shift; $cmd->isa('BleahCommand') or die;
    my $instr = $cmd->getInstructionCode();
    if($instr eq 'PUT') {
      # PUT logic here
    } elsif($instr eq 'GET') {
      # GET logic here
    }
    # etc
  }
 
  # exemple de style "appel de méthode":
  
  sub doCommand {
    my $me = shift;
    my $cmd = shift; $cmd->isa('BleahCommand') or die;
    my $instr = $cmd->getInstructionCode();
    my $func = "process_" . $instr;
    return undef unless defined &$func;
    return $func->($cmd, @_);
  }
 
  # exemple de style "sous-classe".
  # on suppose que %commandHandlers contient une liste de pointeurs d'objets.
 
  sub doCommand {
    my $me = shift;
    my $cmd = shift; $cmd->isa('BleahCommand') or die;
    my $insr = $cmd->getInstructionCode();
    my $objectRef = $commandHandlers{$instr};
    return $objectRef ? $objectRef->handleCommand($cmd, @_) : undef;
  }

Comme Perl dispose d'un AUTOLOAD, le principe pourrait être émulé. Si un package voulait effectuer un ensemble de commandes arbitrairement grand, il pourrait recenser toutes les méthodes undefined grâce à AUTOLOAD, puis tenter de les répartir (ce qui suppose que %commandHandlers contient une table de pointeurs, dont les clés sont les noms des méthodes):

  sub AUTOLOAD {
    my $me = shift;
    (my $methodName) = $AUTOLOAD m/.*::(\w+)$/;
    return if $methodName eq 'DESTROY';
    my $objectRef = $commandHandlers{$methodName};
    return $objectRef ? $objectRef->handleCommand($methodName, @_) : undef;
  }

Cela convertit les appels aux différentes méthodes dans l'objet courant, en appels à une méthode handleCommand dans différents objets. Cet exemple utilise Perl pour adapter un patron de conception à base d'objets Commandes, dans une interface qui en est dépourvue.

abstract class Command
{
    abstract function execute();
}

class Executor
{
    private array $history = [];

    public function saveAndExecute(Command $cmd)
    {
        $this->history[] = $cmd; // optional
        $cmd->execute();
    }
}

class Light
{
    public function turnOn()
    {
        writeLine('Light is on.');
    }

    public function turnOff()
    {
        writeLine('Light is off.');
    }
}

class CommandOn extends Command
{
    public function __construct(private readonly Light $light)
    {
    }

    public function execute()
    {
        $this->light->turnOn();
    }
}

class CommandOff extends Command
{
    public function __construct(private readonly Light $light)
    {
    }

    public function execute()
    {
        $this->light->turnOff();
    }
}

function Client($commandString)
{
    $lamp = new Light();
    $cmdOn = new CommandOn($lamp);
    $cmdOff = new CommandOff($lamp);

    $executor = new Executor();

    switch ($commandString) {
        case 'ON':
            $executor->saveAndExecute($cmdOn);
            break;
        case 'OFF':
            $executor->saveAndExecute($cmdOff);
            break;
        default:
            writeLine('Please ask for "ON" or "OFF" only.');
    }
}

function writeLine($text) {
    print $text.'<br/>';
}

Client('ON');
Client('OFF');

Cela affiche :

Light is on.
Light is off.