Murayama blog.

AIの民主化。

Command

Rubyによるデザインパターン生活。継続中。
本日はCommandパターンを取り上げます。


Rubyによるデザインパターン

Rubyによるデザインパターン


CommandパターンのCommand(コマンド)とは命令の意味です。
この本には、

コマンドはある特定の何かをするための命令です。

と定義されています。


あ、今回からクラス図をアップすることにしました。
それではCommandパターンのクラス図です。


Commandパターンはとてもシンプルな構成になっています。
上記のクラス図では、抽象的なCommandクラス(インタフェース)を実装する、
ConcreteCommand1、ConcreteCommand2クラスを示しています。


Commandインタフェースにはexecuteメソッドが定義されています。
このexecuteメソッドが「ある特定の何かをするための命令」になります。


今回のサンプルに登場するクラスです。

  • Command
    • Commandクラス。名前そのままでCommandパターンのCommandを担当します。
  • CreateFile
    • Commandのサブクラス。ファイルを作成するクラス。
  • DeleteFile
    • Commandのサブクラス。ファイルを削除するクラス。
  • CopyFile
    • Commandのサブクラス。ファイルをコピーするクラス。

※CreateFile、DeleteFile、CopyFileクラスは、ConcreteCommand1(2)クラスのように振る舞います。

  • CompositCommand
    • 複数のコマンドを集約するクラス。自身もCommandのサブクラスであり、保持する複数のコマンドをまとめて実行します。


つづいて、ソースコードです。

require "fileutils"

class Command
  attr_reader :description
  def initialize(description)
    @description = description
  end
  
  def execute
  end
end

class CreateFile < Command
  def initialize(path, contents)
    super("Create file : #{path}")
    @path = path
    @contents = contents
  end
  
  def execute
    f = File.open(@path, "w")
    f.write(@contents)
    f.close
  end
end


class DeleteFile < Command
  def initialize(path)
    super("Delete file : #{path}")
    @path = path
  end
  
  def execute
    File.delete(@path)
  end
end

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

class CompositCommand < Command
  def initialize
    @commands = []
  end
  
  def add_command(cmd)
    @commands << cmd
  end
  
  def execute
    @commands.each { |cmd| cmd.execute }
  end
  
  def description
    description = ""
    @commands.each { |cmd| description += cmd.description + "\n"}
    description
  end
end


cmds = CompositCommand.new
cmds.add_command(CreateFile.new("file1.txt", "hello world\n"))
cmds.add_command(CopyFile.new("file1.txt", "file2.txt"))
cmds.add_command(DeleteFile.new("file1.txt"))

cmds.execute

Commandクラスとそのサブクラスを定義したあと、CompositCommandオブジェクトを生成し、
CreateFile、CopyFile、DeleteFileオブジェクトを追加しています。
その後、追加したCommandオブジェクトを追加した順にまとめて実行しています。


実行結果は、カレントディレクトリに作成したfile1.txtがコピーされてfile2.txtとなります。
また、コピー元となったfile1.txtは削除されます。
結果としてfile2.txtのみが残ります。


また、さきほどのプログラムの最後で、

puts cmds.description

と実行すると、画面に集約したコマンドの一覧を出力します。

Create file : file1.txt
Copy file : file1.txt to file2.txt
Delete file : file1.txt


ここまでの流れをみると、
一つひとつのCommandの処理はexecuteメソッドを実装するだけで単純なものです。
「executeメソッドを実装した」から「Commandパターンだ」と思っていてはいけません。
本に載っている大事な部分を引用します。

Commandパターンのポイントは、何を行うかの決定と、それの実行を分離することです。
このパターンを使う場合、「これを行え」と命令する代わりに、「これを行う方法を記録しろ」と命令し、その後「記録したことを行え」と命令します。

Commandパターンは命令と実行の分離と考えると良いと思います。


また、Commandパターンの応用編として、
Undo(やりなおし)の実装も紹介されています。
さきほどのプログラムの各Commandクラスにunexecuteメソッドを追加してみます。

require "fileutils"

class Command
  attr_reader :description
  def initialize(description)
    @description = description
  end
  
  def execute
  end
end

class CreateFile < Command
  def initialize(path, contents)
    super("Create file : #{path}")
    @path = path
    @contents = contents
  end
  
  def execute
    f = File.open(@path, "w")
    f.write(@contents)
    f.close
  end
  
  def unexecute
    File.delete(@path)
  end
end

class DeleteFile < Command
  def initialize(path)
    super("Delete file : #{path}")
    @path = path
  end
  
  def execute
    if(File.exists?(@path))
      @contents = File.read(@path)
    end
    File.delete(@path)
  end
  
  def unexecute
    f = File.open(@path, "w")
    f.write(@contents)
    f.close
  end
end

class CopyFile < Command
  def initialize(source, target)
    super("Copy file : #{source} to #{target}")
    @source = source
    @target = target
  end
  
  def execute
    if(File.exists?(@target))
      @contents = File.read(@target)
    end
    FileUtils.copy(@source, @target)
  end
  
  def unexecute
    File.delete(@target)
    if(@contents)
      f = File.open(@target, "w")
      f.write(@contents)
      f.close
    end
  end
end

class CompositCommand < Command
  def initialize
    @commands = []
  end
  
  def add_command(cmd)
    @commands << cmd
  end
  
  def execute
    @commands.each { |cmd| cmd.execute }
  end
  
  def unexecute
    @commands.reverse.each { |cmd| cmd.unexecute }
  end
  
  def description
    description = ""
    @commands.each { |cmd| description += cmd.description + "\n"}
    description
  end
end

cmds = CompositCommand.new

cmds.add_command(CreateFile.new("file1.txt", "hello world10\n"))
cmds.add_command(CopyFile.new("file1.txt", "file2.txt"))
cmds.add_command(DeleteFile.new("file1.txt"))

cmds.execute
cmds.unexecute

最後にcmds.unexecuteメソッドを呼び出すことで、
コピー処理の一連の流れを取り消すことができます。