概要
Rubyによるデザインパターン第6章。
Composite Pattern。
Rubyによるデザインパターン5原則に則って理解する。
どんなパターンか
-
あるものが同じような下位のもので作られているという考え方
-
大きなオブジェクトが小さな子オブジェクトから構成されていて、その子オブジェクトもさらに小さな孫オブジェクトでできている
-
階層構造やツリー構造のオブジェクトを作りたい時に利用できる。
登場人物
コンポーネント(Component)
すべてのオブジェクトの共通インターフェイス。もしくは基底クラス。
基本的なオブジェクトや上位のオブジェクトいずれも、必ず共通して共通して持っているもの。
例)ケーキ作成手順を例にとると、タスクの所要時間
葉(Leaf)
プロセスの単純な構成要素で、1つ以上必要。
例)小麦粉の計量や卵の投入など単純タスク
コンポジット(Composite)
コンポーネントの一部だが、サブコンポーネントから作られる、より上位のオブジェクト。
いくつかの小タスクから構成される複合的なタスク。
例)生地の作成など、いくつかの子タスクから構成される複合的なタスク
実例
コンポーネントクラス
全てのタスクに共通する、所要時間取得用の抽象的なget_time_requiredメソッド。
class Task
attr_reader :name
def initialize(name)
@name = name
end
def get_time_required
0.0
end
end
葉クラス
class AddMilkTask < Task
def initialize
super('Add dry ingredients')
end
def get_time_required
1.0 # 砂糖を加えるのに1分
end
end
class MixTask < Task
def initialize
super('Mix that batter up!')
end
def get_time_required
3.0 # 交ぜるのに3分
end
end
コンポジットクラス
葉クラス達からなる複合的作業のクラス
class MakeBatterTask < Task
def initialize
super('Make batter')
@sub_tasks = []
add_sub_task(AddMilkTask.new)
add_sub_task(MixTask.new)
# add_sub_task(AddFreshCreamTask.new)
# etc ..
end
def add_sub_task(task)
@sub_tasks << task
end
def remove_sub_task(task)
@sub_tasks.delete(task)
end
def get_time_required
time=0.0
@sub_tasks.each { |task| time += task.get_time_required }
time
end
end
クラスの利用
make_batter = MakeBatterTask.new
make_batter.get_time_required
=> 4.0
コンポジットクラスの洗練
コンポジットタスクが複数でてくることを見越して、コンポジット用基底クラスを作成
class CompositeTask < Task
def initialize(name)
super(name)
@sub_tasks = []
end
def add_sub_task(task)
@sub_tasks << task
end
def remove_sub_task(task)
@sub_tasks.delete(task)
end
def get_time_required
time=0.0
@sub_tasks.each { |task| time += task.get_time_required }
time
end
end
コンポジットタスク1
バター作成用クラス
class MakeBatterTask < CompositeTask
def initialize
super('Make batter')
add_sub_task(AddMilkTask.new)
add_sub_task(MixTask.new)
# add_sub_task(AddFreshCreamTask.new)
# etc ..
end
end
コンポジットタスク2
ケーキ作成用クラス
→コンポジットタスクであるバター作成クラスを部分として持つ、より上位の複合クラス。
class MakeCakeTask < CompositeTask
def initialize
super('Make cake')
add_sub_task(MakeBatterTask.new)
# add_sub_task(FillPanTask.new)
# add_sub_task(BakeTask.new)
# etc ..
end
end
クラスの利用
make_batter = MakeBatterTask.new
make_batter.get_time_required
=> 4.0
make_cake = MakeCakeTask.new
make_cake.get_time_required
=> 4.0 # MakeCakeTaskにちゃんとsub_taskを追加すればこの値が増える
コンポジットクラスの洗練2
コンポジットオブジェクトは、コレクションとしての役割も持ち合わせている。
(MakeButterは、AddMilkTaskやMixTaskを持つ)
class CompositeTask < Task
def initialize(name)
super(name)
@sub_tasks = []
end
# Array的な要素の追加
def <<(task)
@sub_tasks << task
end
# Array的な値へのアクセス
def [](index)
@sub_tasks[index]
end
# Array的な値の代入
def []=(index, new_value)
@sub_tasks[index] = new_value
end
def remove_sub_task(task)
@sub_tasks.delete(task)
end
def get_time_required
time=0.0
@sub_tasks.each { |task| time += task.get_time_required }
time
end
end
class MakeBatterTask < CompositeTask
def initialize
super('Make batter')
self << AddMilkTask.new
self << MixTask.new
end
end
make_cake = MakeCakeTask.new
make_cake.get_time_required
=> 4.0
# Array的アクセス
make_cake[0]
=> #<MakeBatterTask:0x007fab9dcc9b60
@name="Make batter",
@sub_tasks=
[#<AddMilkTask:0x007fab9dcc9ae8 @name="Add dry ingredients">,
#<MixTask:0x007fab9dcc9a98 @name="Mix that batter up!">]>
# make_cakeのタスクをMakeBatterTaskからMixTaskに変更
make_cake[0] = MixTask.new
=> #<MixTask:0x007faba03c1fb0 @name="Mix that batter up!">
# MixTaskとなり所用時間が減った
make_cake.get_time_required
=> 3.0
部分から全体への参照
全体から部分への参照は、自身のクラスを見れば一目瞭然だが、その逆は現状難しい。
全体に対する参照を扱うコードを追加する。
追加先としては、共通インターフェイスであるコンポーネントクラスが適切。
class Task
attr_accessor :name, :parent
def initialize(name)
@name = name
@parent = nil # 全体(親)情報を保持する属性を追加
end
def get_time_required
0.0
end
end
全体(parent)の属性を設定するのは、上位概念であるコンポジットクラスが適切。
class CompositeTask < Task
def initialize(name)
super(name)
@sub_tasks = []
end
def <<(task)
@sub_tasks << task
task.parent = self # 部分クラス(task)に全体(self)情報を追加
end
def remove_sub_task(task)
@sub_tasks.delete(task)
task.parent = nil # 部分クラス(task)から全体情報を削除
end
def get_time_required
time=0.0
@sub_tasks.each { |task| time += task.get_time_required }
time
end
end
部分から全体への参照を試す
AddMilkTaskとMixTaskからなるMakeBatterTaskを定義
class MakeBatterTask < CompositeTask
def initialize
super('Make batter')
self << AddMilkTask.new
self << MixTask.new
end
end
MakeBatterTaskに追加するためのAddFreshCreamTaskを定義
class AddFreshCreamTask < Task
def initialize
super('Add Fresh Cream')
end
def get_time_required
5.0
end
end
- make_batterタスク(全体)にadd_fresh_cream(部分)を追加
- add_fresh_creamに対する全体(親)を取得
# make_batterを初期化
[12] pry(main)> make_batter = MakeBatterTask.new
[13] pry(main)> make_batter.get_time_required
=> 4.0
# make_batterタスク(全体)にadd_fresh_cream(部分)を追加
[15] pry(main)> add_fresh_cream = AddFreshCreamTask.new
[16] pry(main)> make_batter << add_fresh_cream
[17] pry(main)> make_batter.get_time_required
=> 9.0
# add_fresh_creamに対する全体(親)を取得
[18] pry(main)> add_fresh_cream.parent
=> #<MakeBatterTask:0x007fbd13ac6a98
@name="Make batter",
@parent=nil,
@sub_tasks=
[#<AddMilkTask:0x007fbd13ac69f8 @name="Add dry ingredients", @parent=#<MakeBatterTask:0x007fbd13ac6a98 ...>>,
#<MixTask:0x007fbd13ac69a8 @name="Mix that batter up!", @parent=#<MakeBatterTask:0x007fbd13ac6a98 ...>>,
#<AddFreshCreamTask:0x007fbd1352e388 @name="Add Fresh Cream", @parent=#<MakeBatterTask:0x007fbd13ac6a98 ...>>]>
Compositeパターンの注意点
コンポジット(全体)から葉クラスの数を取得したい時
class CompositeTask < task
~
def total_number_basic_tasks
@sub_tasks.length
end
end
これは間違い。
→コンポジットの構成要素がまたコンポジットである可能性を無視してしまっている。
葉の数を全て数えるには、コンポジットクラスを再帰的に辿る必要がある。
以下2カ所に変更を加える。
class Task
~
# 全ての葉はtotal_number_basic_tasksを保持する。
def total_number_basic_tasks
1
end
end
class CompositeTask < Task
~
# CompositeTaskに各タスクのtotal_number_basic_tasksを取得する本メソッドを追加することで、再帰的に(全ての葉クラスまで辿って)task数を取得する。
def total_number_basic_tasks
@sub_tasks.inject(0) {|sum, task| sum + task.total_number_basic_tasks}
end
end
total_number_basic_tasksの呼び出し
make_batter.total_number_basic_tasks
=> 3
うまくいった。
まとめ
- 一見複雑なオブジェクトたちを再帰的に整理し、変化に強い構造へ。
- ベーシックな考え方であり、他のパターンに紛れて登場することも多い。
以下へ続く
【Iterator】-君の子供たちに伝えたいのだけど-
http://qiita.com/kidachi_/items/afa4c6c29a6eb6be487a