Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第5日目はCommandです。(http://www.amazon.co.jp/gp/product/4894712857/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4894712857&linkCode=as2&tag=morizyun00-22)
5日目 Command
5日目はCommandパターンです。
本書ではGUIアプリケーションの構築が例に挙げられています。
ユーザーがスクリーン上の特定のボタンをクリックすると、on_button_push
メソッドが呼ばれるクラスを構築するときの事例です。
Command サンプルコード
class Button
attr_accessor :command
def initialize(command)
@command = command
end
# ボタンの描画と管理のためのコード
#
#
def on_button_push
@command.execute if @command
end
end
class SaveCommand # コマンドオブジェクト
def execute
# 現在の文書を保存
#
#
end
end
save_button = Button.new(SaveCommand.new)
save_button.on_button_push # => 保存を実行
これがCommandパターンの一例です。
より一般的なButtonクラスを構築し、それに委譲する形でオブジェクトを渡します。
Buttonクラスは不変部分であり、そこに可変のコマンドオブジェクトを実装する形です。
変わるところと変わらないところを分離するというパターン大原則にならっています。
引数にブロックを渡す
クラスではなくコードブロックを渡したいなら、こういった構成にするといいでしょう
※ ただし、常にブロック渡しが有効とも限らないので、クラスベースの委譲方式も選択肢として持っておくべきでしょう!
class Button
attr_accessor :command
def initialize(&command)
@command = command
end
#
#
def on_button_push # ボタンが押されたら
@command.call if @command
end
end
実行コマンドを記録する
require 'fileutils'
class Command # 基底クラス
attr_accessor :description
def initialize(description)
@description = description
end
def execute # 空のexecuteメソッド
end
end
class CreateFile < Command # Create 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 # Delete Command
def initialize(path)
super("Delete file: #{path}")
@path = path
end
def execute
File.delete(@path)
end
end
class CopyFile < Command # Copy Command
include FileUtils
def initialize(source, target)
super("Copy file: #{source} to #{target}")
@source = source
@target = target
end
def execute
FileUtils.copy(@source, @target)
end
end
class CompositeCommand < 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 = CompositeCommand.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
puts cmds.description
実行したコマンドを記録するためにComposite Commandクラスにdescription
メソッドを定義してあります。
def description
description = ''
@commands.each { |cmd| description += cmd.description + "\n" }
description
end
Observerとの違い
- Commandパターン
-
add_command
でコマンドオブジェクトを@commands
配列に登録し、それをイテレータを通して実行します。 - Observerパターン
-
add_observer
でオブザーバオブジェクトを@observers
配列に登録し、それをイテレータを通して実行します。そしてオブザーバオブジェクトは基底クラスの変更を見守っています。
唯一の違いは、登録された側が基底クラス内のデータを監視しているかどうか、だけです。
Commandは渡した処理をただ単に実行するだけです。
コマンドを使ったUndo
単純なUndo機能を実装するのも簡単です
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
file = CreateFile.new('hoge.rb', "何妙法蓮華")
file.execute
# file.unexecute これで簡単にUndoが実行できます
まとめ
実行したい機能を配列として持ち、それを一気に実行したりできるパターンです。
機能を束ねるといった色が強いでしょう。
パターンの構築方法としては Strategy + Composite + Observer = Command のようにも取れるのでしょうか。
Commandは、パターンというよりは概念に近いかもしれません。
またRubyで容易に書くことができるパターンの一つなので、汎用性も高く、使い所は多そうですね。