JavaエンジニアがRubyでデザインパターンを学ぶ - Command Pattern


# 概要
「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。

#### 今までに整理したもの
[Template Method Pattern](http://qiita.com/ksugimori/items/34e766e9bff4cfa62591)
[Strategy Pattern](http://qiita.com/ksugimori/items/f8773909ded7585c5d61)
[Observer Pattern](http://qiita.com/ksugimori/items/cff3501f6ac50120b4db)
[Composite Pattern](http://qiita.com/ksugimori/items/ca00f23ad64779ae3f9a)

# Command Pattern

- オブジェクトに対する命令・要求そのものをオブジェクト化する、というアイデア
- 要求をメソッドではなくオブジェクトとして定義することで、要求の送信と実行のタイミングをずらすことが可能
- 各操作をオブジェクト化することで、操作の履歴の保持や簡易的な Undo 機能に利用できる

## Ruby での実装例

```rb:SqlCommand
# SQL の発行に特化した Command クラス
# 子クラスでは execute, undo で実行したい SQL を定義する
class SqlCommand
  attr_writer :db 
  attr_reader :description

  def initialize(description)
    @description = description
  end

  def execute(sql, *params)
    puts '[ EXECUTE ] ' + @description
    @db.execute(sql, params)
  end

  def undo(sql, *params)
    puts '[  UNDO!  ] ' + @description
    @db.execute(sql, params)
  end
end
```

```rb:CreateTableCommand
class CreateTableCommand < SqlCommand
  def initialize
    super("Create EMPLOYEE table.")
  end

  def execute
    sql = <<-EOS
    CREATE TABLE EMPLOYEE(
    NAME      TEXT    PRIMARY KEY,
    AGE       INT     NOT NULL,
    DIVISION  INT     NOT NULL
    );
    EOS

    super(sql)
  end

  def undo
    super('DROP TABLE EMPLOYEE')
  end
end
```

```rb:InsertCommand
class InsertCommand < SqlCommand
  def initialize(emp)
    super("Insert #{emp} into EMPLOYEE table.")
    @emp = emp
  end

  def execute
    sql = 'INSERT INTO EMPLOYEE (NAME, AGE, DIVISION) VALUES (?, ?, ?)'
    super(sql, @emp[:name], @emp[:age], @emp[:div])
  end

  def undo
    sql = 'DELETE FROM EMPLOYEE WHERE NAME = ?'
    super(sql, @emp[:name])
  end
end
```

```rb:SqlCommandList
class SqlCommandList
  def initialize(db)
    @db = db
    @commands = []
    @history = []
  end

  def <<(command)
    command.db = @db
    @commands << command
  end

  # @commands に保持されているコマンドを全て実行します
  # 複数のDB操作をまとめて１つのトランザクション内で行いたい、という場合も便利
  def execute_all
    @db.transaction do
      @commands.each { |c| c.execute }
    end

    @history.concat(@commands)
    @commands.clear
  end

  # 実行済みのコマンドをひとつ巻き戻し、履歴を削除します
  def undo
    @history.pop.undo
  end
end
```


```rb:呼び出し
require 'sqlite3'

command_list = SqlCommandList.new(SQLite3::Database.new('sample.db'))
command_list << CreateTableCommand.new

command_list << InsertCommand.new(name: 'Alice'  , age: 24, div: 'Development'    )
command_list << InsertCommand.new(name: 'Bob'    , age: 32, div: 'Human Resources')
command_list << InsertCommand.new(name: 'Charlie', age: 26, div: 'Sales'          )
command_list << InsertCommand.new(name: 'Daisuke', age: 20, div: 'Sales'          )

command_list.execute_all # この時点で初めてSQLが発行される。
gets # 一次停止して DB の状況を確認する - 1

command_list.undo
command_list.undo
gets # 一次停止して DB の状況を確認する - 2

command_list.undo
command_list.undo
command_list.undo
gets # 一次停止して DB の状況を確認する - 3
```

###実行結果

```
$ ruby main.rb
[ EXECUTE ] Create EMPLOYEE table.
[ EXECUTE ] Insert {:name=>"Alice", :age=>24, :div=>"Development"} into EMPLOYEE table.
[ EXECUTE ] Insert {:name=>"Bob", :age=>32, :div=>"Human Resources"} into EMPLOYEE table.
[ EXECUTE ] Insert {:name=>"Charlie", :age=>26, :div=>"Sales"} into EMPLOYEE table.
[ EXECUTE ] Insert {:name=>"Daisuke", :age=>20, :div=>"Sales"} into EMPLOYEE table.

[  UNDO!  ] Insert {:name=>"Daisuke", :age=>20, :div=>"Sales"} into EMPLOYEE table.
[  UNDO!  ] Insert {:name=>"Charlie", :age=>26, :div=>"Sales"} into EMPLOYEE table.

[  UNDO!  ] Insert {:name=>"Bob", :age=>32, :div=>"Human Resources"} into EMPLOYEE table.
[  UNDO!  ] Insert {:name=>"Alice", :age=>24, :div=>"Development"} into EMPLOYEE table.
[  UNDO!  ] Create EMPLOYEE table.
```

`gets` で処理を中断している間に DB を確認。

```sqlite3
$ sqlite3 sample.db
SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite> .header on
sqlite> .width 10 10 18
sqlite> .mode column
sqlite>
sqlite> select * from employee;
NAME        AGE         DIVISION
----------  ----------  ------------------
Alice       24          Development
Bob         32          Human Resources
Charlie     26          Sales
Daisuke     20          Sales
sqlite>
sqlite> select * from employee;
NAME        AGE         DIVISION
----------  ----------  ------------------
Alice       24          Development
Bob         32          Human Resources
sqlite>
sqlite> select * from employee;
Error: no such table: employee
```


## Rubyでの実装例（より Ruby らしく）

Undo 機能などが不要で、各コマンドがシンプルならば Command クラスを定義せずブロックとして処理を渡した方がシンプルに記述できます。他のパターンでもそうでしたが、やりたいことがブロックだけで表現できる場合はブロックを使った方が Ruby らしいコードになりますね。

```rb:SqlCommandList
class SqlCommandList
  def initialize(db)
    @db = db
    @commands = []
  end

  def add(&block)       # コマンド追加メソッドはブロックとして受け取り
    @commands << block
  end

  # @commands に保持されているコマンドを全て実行します
  def execute_all
    @db.transaction do
      @commands.each { |c| c.call(@db) } # call で処理を呼び出し
    end

    @commands.clear
  end
end
```

```rb:呼び出し
require 'sqlite3'

command_list = SqlCommandList.new(SQLite3::Database.new('sample.db'))
command_list.add do |db|
  sql = <<-EOS
  CREATE TABLE EMPLOYEE(
  NAME      TEXT    PRIMARY KEY,
  AGE       INT     NOT NULL,
  DIVISION  INT     NOT NULL
  );
  EOS

  db.execute(sql)
end

sql = 'INSERT INTO EMPLOYEE (NAME, AGE, DIVISION) VALUES (?, ?, ?)'
command_list.add {|db| db.execute(sql, 'Alice'  , 24, 'Development'    ) }
command_list.add {|db| db.execute(sql, 'Bob'    , 32, 'Human Resources') }
command_list.add {|db| db.execute(sql, 'Charlie', 26, 'Sales'          ) }
command_list.add {|db| db.execute(sql, 'Daisuke', 20, 'Sales'          ) }

command_list.execute_all
```

# 参考
Olsen, R. 2007. [*Design Patterns in Ruby*](https://www.amazon.co.jp/dp/B004YW6M6G/ref=cm_sw_r_tw_dp_JODHxb1S34F51)