LoginSignup
2
3

More than 5 years have passed since last update.

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

Posted at

概要

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

今までに整理したもの

Template Method Pattern
Strategy Pattern
Observer Pattern
Composite Pattern

Command Pattern

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

Ruby での実装例

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
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
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
SqlCommandList
class SqlCommandList
  def initialize(db)
    @db = db
    @commands = []
    @history = []
  end

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

  # @commands に保持されているコマンドを全て実行します
  # 複数のDB操作をまとめて1つのトランザクション内で行いたい、という場合も便利
  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
呼び出し
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 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 らしいコードになりますね。

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
呼び出し
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

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3