Ruby
デザインパターン

「HeadFirstデザインパターン」と「Rubyによるデザインパターン」を読んで Command パターン

何番煎じか判りませんがお勉強メモを残します

HeadFirstデザインパターン」第6章
Rubyによるデザインパターン」第8章

Command パターン

「HeadFirstデザインパターン」でのJavaコードは(だいたい)こんな感じ

  • 様々な機能をボタンに割り当てられるリモコンを実装する
  • ボタンは on/off がセットになっている。今回の例では7セットある
  • 最後に押した機能を取り消すアンドゥボタンが1つある
  • 各機能はベンダから(Javaのクラスとして)提供される

書籍内では

public class RemoteControl {
  public void onButton0WasPushed() {
    if (settedFunction == Light) {
      light.on();
    }
    else if(settedFunction == Tv) {
      tv.on();
    }
    else {
      // ....
    }
  }
}

のようにすると アカン事になる予感がする、
新しいベンダクラスが追加されたり、設定を変える時にボタンに関わる全てのコードを調べなければいけなくなりそうだ、
リモコン(ボタン)はベンダクラスの詳細を知っているようにはしたくない、
というような会話が行われる

いまこそ Command パターン の出番だ!

public interface Command {
  public void execute();
}
public class LightOnCommand implements Command {
  Light light;

  public LightOnCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.on();
  }
}
public class SimpleRemoteControl {
  Command slot;

  public SimpleRemoteControl() {}

  public void setCommand(Command command) {
    slot = command;
  }

  public void buttonWasPushed() {
    slot.execute();
  }
}
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light(); // Lightはベンダクラス(仕様によるとon/offメソッドがあるらしい・・みたいな設定)
LightOnCommand lightOn = new LightOnCommand(light);

remote.setCommand(lightOn);
remote.buttonWasPushed();

Command パターン はリクエストをオブジェクトとしてカプセル化する

今回の例だと
SimpleRemoteControl はボタンが押されたならコンストラクタで指定されたオブジェクト(Commandオブジェクトらしいが、それ以外は関知しない) の execute メソッドを呼び出す
LightOnCommand は execute メソッドが呼ばれたならコンストラクタで指定されたオブジェクト(Lightオブジェクトらしいが、それ以外は関知しない) の on メソッドを呼び出す
リモコン - リクエスト - コマンド が 分離されている

ボタンが一つしかなさそうなシンプルなリモコンを実装したが
次は複数のボタンが定義されているリモコンを実装する

public class noCommand implements Command {
  public void execute() {}
}

public class LightOnCommand implements Command {
  Light light;

  public LightOnCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.on();
  }
}

public class LightOffCommand implements Command {
  Light light;

  public LightOffCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.off();
  }
}

public class StereoOnWithCDCommand implements Command {
  Stereo stereo;

  public StereoOnWithCDCommand(Stereo stereo) {
    this.stereo = stereo;
  }

  public void execute() {
    stereo.on();
    stereo.setCD();
    stereo.setVolume(10);
  }
}
public class RemoteControl {
  Command[] onCommands;
  Command[] offCommands;

  public RemoteControl() {
    onCommands  = new Command[7];
    offCommands = new Command[7];

    // 「何もしない」コマンドを最初に割り当てておく
    Command noCommand = new Nocommand();

    for (int = 0; i < 7; i++) {
      onCommands[i]  = noCommand;
      offCommands[i] = noCommand;
    }
  }

  public void setCommand(int slot, Command onCommand, Command offCommand) {
    onCommands[slot]  = onCommand;
    offCommands[slot] = offCommand;
  }

  public void onButtonWasPushed(int slot) {
    onCommaneds[slot].execute();
  }

  public void offButtonWasPushed(int slot) {
    offCommaneds[slot].execute();
  }
}
RemoteControl remote = new RemoteControl();

Light light   = new Light(); 
Stereo stereo = new Stereo(); 

LightOnCommand        lightOn  = new LightOnCommand(light);
LightOffCommand       lightOff = new LightOffCommand(light);
StereoOnWithCDCommand stereoOn = new StereoOnWithCDCommand(light);
//他にもたくさん・・

remote.setCommand(0, lightOn, lightOff);
remote.setCommand(1, stereoOn, stereoOff);

// ボタンを押してみる
remote.onButtonWasPushed(0);
remote.offButtonWasPushed(0);
remote.onButtonWasPushed(1);

アンドゥボタンが押下された時の処理を実装する
アンドゥボタンが2回続けて押下されたときの事は考えていない。やりたかったらStackに積んでおけば実装できます

public interface Command {
  public void execute();
  public void undo(); // 新たにundoを実装する
}

public class LightOnCommand implements Command {
  Light light;

  public LightOnCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.on();
  }

  public void undo() {
    light.off(); // executeとは逆のことをする
  }
}

public class LightOffCommand implements Command {
  Light light;

  public LightOffCommand(Light light) {
    this.light = light;
  }

  public void execute() {
    light.off();
  }

  public void undo() {
    light.on(); // executeとは逆のことをする
  }
}

public class RemoteControl {
  Command[] onCommands;
  Command[] offCommands;
  Command undoCommand; // 最後に実行したコマンドを格納する

  public RemoteControl() {
    onCommands  = new Command[7];
    offCommands = new Command[7];

    // 「何もしない」コマンドを最初に割り当てておく
    Command noCommand = new Nocommand();

    for (int = 0; i < 7; i++) {
      onCommands[i]  = noCommand;
      offCommands[i] = noCommand;
    }

    undoCommand = noCommand; // 他のボタンと同じで初期値は何もしない
  }

  public void setCommand(int slot, Command onCommand, Command offCommand) {
    onCommands[slot]  = onCommand;
    offCommands[slot] = offCommand;
  }

  public void onButtonWasPushed(int slot) {
    onCommaneds[slot].execute();
    undoCommand = onCommaneds[slot];  // ボタンが押されたらそのコマンドを覚えておく
  }

  public void offButtonWasPushed(int slot) {
    offCommaneds[slot].execute();
    undoCommand = offCommaneds[slot];  // ボタンが押されたらそのコマンドを覚えておく
  }

  public void undoButtonWasPushed() { // undoボタンが押されたらundoCommandに格納されている最後に実行したコマンドのundoを呼び出す
    undoCommand.undo();
  }
}
RemoteControl remote = new RemoteControl();

Light light   = new Light(); 
Stereo stereo = new Stereo(); 

LightOnCommand        lightOn  = new LightOnCommand(light);
LightOffCommand       lightOff = new LightOffCommand(light);
StereoOnWithCDCommand stereoOn = new StereoOnWithCDCommand(light);
//他にもたくさん・・

remote.setCommand(0, lightOn, lightOff);

// ボタンを押してみる
remote.onButtonWasPushed(0);
remote.undoButtonWasPushed();

Lightのundoは、onとoffで単に逆の事をするだけでしたが
コマンドによっては例えば「ファンの強さを弱から強にした」のundoを行うために、「以前は弱だった」という事を保存していなければなりません
コマンド内のインスタンス変数に「弱」を保存しておき、undoメソッドからそのインスタンス変数を参照し「ファンの強さを以前の弱状態に戻す」ような処理も必要になることもあると思います

次は複数のコマンドを一つのコマンドにまとめて実行、そんな事を試してみます

public class MacroCommand implements Command {
  Command[] commands;

  public MacroCommand(Command[] commands) {
    this.commands = commands;
  }

  public void execute() {
    for (int i = 0, l = commands.length; i < l; i++) {
      commands[i].execute();
    }
  }

  public void undo() {
    for (int i = 0, l = commands.length; i < l; i++) {
      commands[i].undo();
    }
  }
}
RemoteControl remote = new RemoteControl();

Light  light  = new Light(); 
Stereo stereo = new Stereo(); 
TV     tv     = new TV(); 

LightOnCommand        lightOn   = new LightOnCommand(light);
LightOffCommand       lightOff  = new LightOffCommand(light);
StereoOnWithCDCommand stereoOn  = new StereoOnWithCDCommand(stereo);
StereoOffCommand      stereoOff = new StereoOff(light);
TVOnCommand           tvOn      = new TVOnCommand(tv);
TVOffCommand          tvOff     = new TVOffCommand(tv);

MacroCommand partyOn  = [lightOn,  stereoOn,  tvOn];
MacroCommand partyOff = [lightOff, stereoOff, tvOff];

remote.setCommand(0, partyOn, partyOff);

// ボタンを押してみる
remote.onButtonWasPushed(0);
remote.undoButtonWasPushed();

「Rubyによるデザインパターン」でのコードは(だいたい)こんな感じ

Javaと言いたいことはあまり変わらないので面白い例ではない
「何を行うか」と「実行部分」を分離する
時間の経過とともに多くの操作を蓄え、一気に実行させたいときにもCommand パターンは有効

※ ごめんなさい、実質的にCommandパターンに関係ない部分は省略しました

class Command
  attr_reader :description

  def initialize(description)
    @description = description
  end

  def execute
  end
end

class CompositeCommand < Command
  def initialize
    @commands = []
  end

  def add_command(command)
    @commands << command
  end

  def execute
    @commands.each(&:execute)
  end

  def unexecute
    @commands.reverse.each(&:unexecute)
  end

  def description
    @commands.map(&:description).join("\n")
  end
end

class CreateFile < Command
  def initialize(path, contents)
    super("Create file #{path}")
    @path     = path
    @contents = contents
  end

  def execute
    # unexecuteの為に
    # 「既存ファイルがあるならその内容を保存」しておく。省略

    File.open(@path, 'w') do |file|
      file.write(@contents)
    end
  end

  def unexecute
    # execute時に
    # 既存ファイルがあったならその内容を復元し
    # 無かったならファイルを削除する。省略
  end
end

require 'fileutils'

class CopyFile < Command
  def initialize(source, target)
    super("Copy file #{source} to #{target}")
    @source = source
    @target = target
  end

  def execute
    # unexecuteの為に
    # 「コピー先に既存ファイルがあるならその内容を保存」しておく。省略

    FileUtils.copy(@source, @target)
  end

  def unexecute
    # execute時に
    # コピー先に既存ファイルがあったならその内容を復元し
    # 無かったならファイルを削除する。省略
  end
end

class DeleteFile < Command
  def initialize(path)
    super("Delete file #{path}")
    @path = path
  end

  def execute
    # unexecuteの為に
    # 内容を保存しておく。省略

    File.delete(@path)
  end

  def unexecute
    # execute時に削除した内容を復元する。省略
  end
end

cmds = CompositeCommand.new

cmds.add_command(CreateFile.new('delete_me.txt', 'aaaaa'))
cmds.add_command(CopyFile.new('delete_me.txt', 'delete_me2.txt'))
cmds.add_command(DeleteFile.new('delete_me.txt'))
cmds.add_command(DeleteFile.new('delete_me2.txt'))

cmds.description
# Create file delete_me.txt
# Copy file delete_me.txt to delete_me2.txt
# Delete file delete_me.txt
# Delete file delete_me2.txt

cmds.execute

cmds.unexecute