Help us understand the problem. What is going on with this article?

Rubyによるデザインパターン【Composite】 -世界は再帰的(部分は全体、全体は部分)-

More than 5 years have passed since last update.

概要

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした