何番煎じか判りませんがお勉強メモを残します
「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 パターンがよく合う