LoginSignup
1
0

More than 5 years have passed since last update.

「HeadFirstデザインパターン」と「Rubyによるデザインパターン」を読んで Composite パターン

Posted at

何番煎じか判りませんがお勉強メモを残します

HeadFirstデザインパターン」第9章
Rubyによるデザインパターン」第6章

Composite パターン

「HeadFirstデザインパターン」でのJavaコードは(だいたい)こんな感じ

  • パンケーキメニュー
  • 食堂メニュー
  • カフェメニュー

を表示します。現状はこんな感じです

Iterator pancakeIterator = getPancakeIterator();
Iterator dinerIterator   = getDinerIterator();
Iterator cafeIterator    = getCafeIterator();

System.out.println("パンケーキメニュー");
printMenu(pancakeIterator);

System.out.println("食堂メニュー");
printMenu(dinerIterator);

System.out.println("カフェメニュー");
printMenu(cafeIterator);

メニューが増えるたびにソースをコピペしないとイケない感がすごい
これは普通どうにかしたくなる

public void printMenus(menus) {
  while(menus.hasNext()) {
    Iterator menu = menus.next();
    printMenu(menu);
  }
}

ArrayList menus = new ArrayList();

menus.add(getPancakeIterator());
menus.add(getDinerIterator());
menus.add(getCafeIterator());

printMenus(menus)

誰だってそーする おれもそーする

が、上司に
「食堂メニューの下にサブメニューとしてデザートメニューを持つ事になった。チャッチャと修正よろしく」
と言われた時の衝撃は皆さま経験があるかと思います

いまこそ Composite パターン の出番だ!

部分-全体構造を表現する為にオブジェクトをツリー構造に構成できる
子要素を持つ要素をノードと呼び、子要素を持たない要素をリーフ と呼んだりする
ノードとリーフ、両方に対して同じ操作を適用できる 両者の違いを無視できる

コンポジション?コンポーネント?コンポジット?
何だか判らないと思うので「ツリー構造のアレ」くらいに思っていればいいと思います

public abstract class MenuComponent {
  // 「コンポジット」メソッド
  public void add(MenuComponent menuComponent) {
    throw new NoMethodError();
  }
  public void delete(MenuComponent menuComponent) {
    throw new NoMethodError();
  }
  public MenuComponent getChild(int i) {
    throw new NoMethodError();
  }

  // 「操作」メソッド
  public String getName() {
    throw new NoMethodError();
  }
  public String getDescription() {
    throw new NoMethodError();
  }
  public double getPrice() {
    throw new NoMethodError();
  }

  // Menu, ItemMenu 両方が実装する「操作」メソッド
  public void print() {
    throw new NoMethodError();
  }
}
// ノード
public class Menu extends MenuComponent {
  ArrayList menuComponents = new ArrayList();
  String name;
  String description;

  public Menu(String menu, String description) {
    this.name        = name;
    this.description = description;
  }

  public void add(MenuComponent menuComponent) {
    menuComponents.add(menuComponent);
  }
  public void delete(MenuComponent menuComponent) {
    menuComponents.remove(menuComponent);
  }
  public MenuComponent getChild(int i) {
    return (MenuComponent)menuComponents.get(i);
  }

  public String getName() {
    return name;
  }
  public String getDescription() {
    return description;
  }

  public void print() {
    System.out.println("");
    System.out.println(getName());
    System.out.println(getDescription());
    System.out.println("↓サブメニュー");

    Iterator iterator = menuComponents.iterator();

    while (iterator.hasNext()) {
      MenuComponent menuComponent = (MenuComponent)iterator.next();
      menuComponent.print();
    }
  }
}
// リーフ
public class MenuItem extends MenuComponent {
  String name;
  String description;
  double price;

  public MenuItem(String menu, String description, double price) {
    this.name        = name;
    this.description = description;
    this.price       = price;
  }

  public String getName() {
    return name;
  }
  public String getDescription() {
    return description;
  }
  public double getPrice() {
    return price;
  }

  public void print() {
    System.out.println("");
    System.out.println(getName());
    System.out.println(getDescription());
    System.out.println(getPrice());
  }
}
MenuComponent allMenus = new Menu("全メニュー", "すべてを統合したメニュー");

MenuComponent pancakeMenu = new Menu("パンケーキメニュー", "パンケーキ説明");
MenuComponent dinerMenu   = new Menu("食堂メニュー", "食堂説明");
MenuComponent CafeMenu    = new Menu("カフェメニュー", "カフェ説明");
MenuComponent dessertMenu = new Menu("デザートメニュー", "デザート説明");

pancakeMenu.add(new MenuItem("パンケーキメニュー1", "説明", 1.1))
dinerMenu.add(new MenuItem("食堂メニュー1", "説明", 2.1))
CafeMenu.add(new MenuItem("カフェメニュー1", "説明", 3.1))
dessertMenu.add(new MenuItem("デザートメニュー1", "説明", 4.1))

allMenus.add(pancakeMenu);
allMenus.add(dinerMenu);
allMenus.add(CafeMenu);

dinerMenu.add(dessertMenu); // 孫Menuになる

allMenus.print();
// ""
// "全メニュー"
// "すべてを統合したメニュー"
// "↓サブメニュー"
// ""
// "パンケーキメニュー"
// "パンケーキ説明"
// "↓サブメニュー"
// ""
// "パンケーキメニュー1"
// "説明"
// "1.1"
// ""
// "食堂メニュー"
// "食堂説明"
// "↓サブメニュー"
// ""
// "デザートメニュー"
// "デザート説明"
// ""
// "デザートメニュー1"
// "説明"
// "4.1"
// ""
// "食堂メニュー1"
// "説明"
// "2.1"
// ""
// "カフェメニュー"
// "カフェ説明"
// ""
// "カフェメニュー1"
// "説明"
// "3.1"

ノードとリーフ、区別できない(しなくてもよい)ようにするものだと上に書きましたが
クライアントからはそう見えるだけで、ノードとリーフは結局のところ別クラスです
だからこそ今回の例では
Menu(ノード)にはpriceは設定できない(必要がない)し、
MenuItem(リーフ)には小要素は追加できない(必要がない)
のですし、誤った操作をしてしまう心配がないとも言えます

あと、子要素を取得している箇所は省略しています
自分がノードなら「子要素を反復処理するIterator」
自分がリーフなら「has_next()が必ずfalseを返すIterator」
を使用する感じです

Rubyではどうなるんや?

「Rubyによるデザインパターン」でのコードは(だいたい)こんな感じ

ケーキを作る手順を細分化する
大きく分けて ケーキを焼く手順・パッケージする手順 等に分かれている
ケーキを焼く手順は バターを作る手順・焼く手順 等に分かれている
つまり、ケーキを焼く手順はツリー構造で考える事ができる
てきとーに細分化したのが下図である(書籍よりてきとーに減らしました)

- Manufacture Cake
  - Make Cake
    - Make Batter
    - AddDryIngredientsTask
    - Bake
  - Package Cake
    - Box

ひとつひとつの手順を個々のクラスにモデル化します
すべての手順に、作業時間を報告するメソッド(time_required)を実装します
ノードのtime_requiredは、自分の子要素のtime_requiredを合計します(これがミソです)
この段では "Make Cake" のツリーを作ってみます

class Task
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def time_required
    0.0
  end
end
class MakeBatter < Task
  def initialize
    super('Make batter task')
  end

  def time_required
    1.0
  end
end

class AddDryIngredientsTask < Task
  def initialize
    super('Add dry ingredients task')
  end

  def time_required
    2.0
  end
end

class BakeTask < Task
  def initialize
    super('Bake task')
  end

  def time_required
    3.0
  end
end
class MakeCakeTask < Task
  def initialize
    super('Make cake task')
    @sub_tasks = []
    add_sub_task(MakeBatter.new) 
    add_sub_task(AddDryIngredientsTask.new) 
    add_sub_task(BakeTask.new)
  end

  def add_sub_task(task)
    @sub_tasks << task
  end

  def delete_sub_task(task)
    @sub_tasks.delete(task)
  end

  def time_required
    @sub_tasks.sum(&:time_required)
  end
end
MakeCakeTask.new.time_required
# => 6.0

BakeTask.new.time_required
# => 3.0

"子task管理の処理は基底クラスに移したほうが良いでしょう" との事なので、その通りにします
ノードは Task を継承して
リーフは CompositeTask < Task を継承する
という事ですね
"Manufacture Cake" からのすべてのツリーを実装してみます

class Task
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def time_required
    0.0
  end
end
class CompositeTask < Task
  def initialize(name)
    super(name)
    @sub_tasks = []
  end

  def add_sub_task(task)
    @sub_tasks << task
  end

  def delete_sub_task(task)
    @sub_tasks.delete(task)
  end

  def time_required
    @sub_tasks.sum(&:time_required)
  end
end
class MakeBatter < Task
  def initialize
    super('Make batter task')
  end

  def time_required
    1.0
  end
end

class AddDryIngredientsTask < Task
  def initialize
    super('Add dry ingredients task')
  end

  def time_required
    2.0
  end
end

class BakeTask < Task
  def initialize
    super('Bake task')
  end

  def time_required
    3.0
  end
end

class Box < Task
  def initialize
    super('Bake task')
  end

  def time_required
    4.0
  end
end
class MakeCakeTask < CompositeTask
  def initialize
    super('Make cake task')
    add_sub_task(AddDryIngredientsTask.new) 
    add_sub_task(BakeTask.new)
  end
end

class PackageCakeTask < CompositeTask
  def initialize
    super('Package cake task')
    add_sub_task(Box.new) 
  end
end
class ManufactureCakeTask < CompositeTask
  def initialize
    super('Manufacture cake task')
    add_sub_task(MakeCakeTask.new) 
    add_sub_task(PackageCakeTask.new) 
  end
end
ManufactureCakeTask.new.time_required
# => 10.0

MakeCakeTask.new.time_required
# => 6.0

BakeTask.new.time_required
# => 3.0

composite パターンは再帰的な性質を理解すれば思ったよりも単純
全体がグループとよく似ている場合、composite パターンがよく合う

1
0
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
1
0