Le motif command

Définition

Le motif commande est le principe d’encapsuler des requêtes (au sens large, pas forcement SQL). Cela permet au client de construire ça demande.

Principe

Un objet sert de télécommande universel, on lui donneras les diverse commande dont on a besoin. Il s’occuperas de gérer la pile d’execution, sont historique, et l’execution à proprement parlé.

L’exemple par le code

Nous allons poser les bases d’un pseudo logiciel de dessin.

Voyons l’objet principal qui regrouperas les commandes.

class Drawer
  attr :command_list
  def initialize
    @command_list = {}
    @last_command = []
  end
  def do(command)
    if @command_list[command] != nil then
      @last_command.push(@command_list[command])
      @command_list[command].do.call
    end
    else 
      #~ raise Exception.new("la commande '#{command}' n'est pas enregistré")
      puts "la commande '#{command}' n'est pas enregistré" 
    end
  end
  def undo
    if !@last_command.empty?
      @last_command.pop.undo.call
    else 
      #~ raise Exception.new("plus de command à annuler")
      puts "plus de command à annuler" 
    end
  end
end

Bien. Dans l’initialisation, on prépare deux listes. L’une nous servira à stocké l’historique des commandes, l’autre contriendra les commandes enregistrées. Noté les deux lignes en commentaire. Selon votre besoin, vore envie, plutôt que de renvoyer un message comme nous le faisons là, vous pourriez lever une exception.

Voyons maintenant deux commandes qui nous servirons de base.

class Trace_line
  def Trace_line.do
    return proc {puts "je trace une ligne"}
  end
  def Trace_line.undo
    return proc {puts "j'efface la dernière ligne"}
  end
end
class Trace_round
  attr :radius
  def initialize(radius)
    @radius = radius
  end
  def do
    return proc {puts "je trace un un rond de rayon: #{@radius}"}
  end
  def undo
    return proc {puts "j'efface le dernier rond de rayon : #{@radius}"}
  end
end

Ces deux objets ne sont pas forcement très logique, mais disons que ça nous permet de voir un cas de figure interessant. Pour la ligne, je n’ai pas mis d’attribut. Du coup pas besoin de faire une instance, d’ou l’utilisation de methode dites de classe.

Par contre, et ça permet de voir qu’on peut définir ce que l’on souhaite comme commande, l’objet Trace_round à lui un attribut: son rayon. Nous allons maintenant voir comment cela se passe en testant tout ça.

draw = Drawer.new
draw.command_list["line"] = Trace_line
draw.command_list["round"] = Trace_round.new(12)
draw.do("line")
draw.do("round")
draw.do("toto")
draw.undo
draw.undo
draw.undo

Bien, nous avons créé un objet Drawer, et nous lui glissons nos deux commandes. Trace_line est passer sous forme de classe alors que Trace_round lui est passer sous forme d’objet. C’est l’utilisation de l’objet ruby Proc qui permet cette souplesse, merci Ruby ! Ensuite nous demandons à notre objet d’executer les commandes, dont une qui n’a pas été enregistrée, et de défaire 3 commandes alors que nous n’en avons lancé que 2 valide… Voilà le résultat:

je trace une ligne
je trace un un rond de rayon: 12
la commande 'toto' n'est pas enregistré
j'efface le dernier rond de rayon : 12
j'efface la dernière ligne
plus de command à annuler

Et voilà, les débuts du remplaçant de the Gimp ;-)

Attention: les librairies de type Marshall pour la sérialisation ne peuvent sérialiser les objets Proc. Ceci dit nous pourrions les remplacer par de vrai objets…