概要
「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