《Head First 設計模式》:組合模式
正文
一、定義
組合模式允許你將對象合成樹形結構來表現「整體/部分」層次結構。組合能讓客戶以一致的方式處理組合對象以及個體對象。
- 組合對象:包含其他組件的組件。
- 個體對象(葉節點對象):沒有包含其他組件的組件。
要點:
- 組合結構內的任意對象稱為組件,組件可以是組合,也可以是葉節點。
- 通過將組合對象和個體對象放在樹形結構中,我們創建了一個「整體/部分」層次結構。如果將整個樹形結構視為一個「大組合」的話,那麼這個樹形結構的每一個「子樹形結構」也是一個組合,包括葉節點也可以被視為一個不包含其他對象的組合。這樣一來,我們就有了「以一致的方式處理」的基礎了。
- 所謂「以一致的方式處理」,是指組合和葉節點具有共同的方法可以調用。這就要求它們必須實現相同的介面。
- 組合模式允許客戶對組合對象和個體對象一視同仁。換句話說,我們可以把相同的操作應用在組合對象和個體對象上。
二、實現步驟
1、創建組件抽象類
也可以使用組件介面。
組件中有些方法可能不適合某種對象,此時我們可以拋異常或者提供特定實現。
/**
* 組件抽象類
*/
public abstract class Component {
/**
* 子組件(可以是組合或葉節點)
*/
protected List<Component> childs = new ArrayList<Component>();
/**
* 添加子組件
*/
public void addChild(Component component) {
childs.add(component);
}
/**
* 移除子組件
*/
public void removeChild(Component component) {
childs.remove(component);
}
/**
* 獲取所有子組件
*/
public List<Component> getChilds() {
return childs;
}
public String getName() {
// 默認拋異常,由子類決定要不要覆蓋
throw new UnsupportedOperationException();
}
}
2、創建組合及葉節點,並繼承組件抽象類
(1)組合
組合可以包含其他組合,也可以包含葉節點。
/**
* 組合
*/
public class Composite extends Component {
private String name;
public Composite(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
(2)葉節點
葉節點無法添加、刪除、獲取子節點,因此需要對相應的方法進行特殊處理。
/**
* 葉節點
*/
public class Leaf extends Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void addChild(Component component) {
// 葉節點不能添加子節點,可以拋異常或者空實現
throw new UnsupportedOperationException();
}
@Override
public void removeChild(Component component) {
// 葉節點沒有子節點可移除,可以拋異常或者空實現
throw new UnsupportedOperationException();
}
@Override
public List<Component> getChilds() {
// 葉節點沒有子節點,可以拋異常或者返回空集合
throw new UnsupportedOperationException();
}
@Override
public String getName() {
return name;
}
}
3、統一使用組件的方法,操作組合及葉節點
由於組合及葉節點都實現了組件介面,因此可以使用組件的方法來操作組合及葉節點。
public class Test {
public static void main(String[] args) {
// 組合
Component composite1 = new Composite("composite1");
Component composite2 = new Composite("composite2");
Component composite3 = new Composite("composite3");
// 葉節點
Component leaf1 = new Leaf("leaf1");
Component leaf2 = new Leaf("leaf2");
Component leaf3 = new Leaf("leaf3");
Component leaf4 = new Leaf("leaf4");
// 組合1包含組合2、3
composite1.addChild(composite2);
composite1.addChild(composite3);
// 組合2包含葉節點1、2
composite2.addChild(leaf1);
composite2.addChild(leaf2);
// 組合3包含葉節點3、4
composite3.addChild(leaf3);
composite3.addChild(leaf4);
// 列印組件名稱
System.out.println(composite1.getName());
for (Component child : composite1.getChilds()) {
System.out.println(" " + child.getName());
for (Component leaf : child.getChilds()) {
System.out.println(" " + leaf.getName());
}
}
}
}
三、舉個栗子
1、背景
對象村餐廳和對象村煎餅屋合併了,它們合併後的新公司創建了一個 Java 版本的女招待。這個 Java 版本的女招待能夠列印使用 ArrayList 存儲的餐廳菜單和煎餅屋菜單。
現在它們打算加上一份餐後甜點的「子菜單」。也就是說,這個 Java 版本的女招待不僅要支援列印多個菜單,還要支援列印菜單中的菜單。
2、實現
由於菜單可能包含菜單和菜單項,因此我們可以創建一個樹形結構,這個樹形結構由菜單和菜單項組成。
通過將菜單和菜單項放在相同的結構中,我們既可以把整個結構視為一個「大菜單」,也可以把這個結構的任一部分視為一個「子菜單」,包括菜單項也可以視為一個「我本身就是菜單項因此沒必要再包含菜單項的菜單」。這樣,我們就可以以一致的方式來處理菜單和菜單項了。
(1)創建菜單組件抽象類
/**
* 菜單組件抽象類
*/
public abstract class MenuComponent {
/**
* 子組件(可以是菜單或菜單項)
*/
protected List<MenuComponent> childs = new ArrayList<MenuComponent>();
/**
* 添加子組件
*/
public void addChild(MenuComponent component) {
childs.add(component);
}
/**
* 移除子組件
*/
public void removeChild(MenuComponent component) {
childs.remove(component);
}
/**
* 獲取所有子組件
*/
public List<MenuComponent> getChilds() {
return childs;
}
public String getName() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
(2)創建菜單,並繼承菜單組件抽象類
/**
* 菜單(組合)
*/
public class Menu extends MenuComponent {
private String name;
public Menu(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void print() {
System.out.println("\n" + getName());;
System.out.println("---------------------");;
}
}
(3)創建菜單項,並繼承菜單組件抽象類
/**
* 菜單項(葉節點)
*/
public class MenuItem extends MenuComponent {
private String name;
private double price;
public MenuItem(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public void addChild(MenuComponent component) {
throw new UnsupportedOperationException();
}
@Override
public void removeChild(MenuComponent component) {
throw new UnsupportedOperationException();
}
@Override
public List<MenuComponent> getChilds() {
return childs;
}
@Override
public String getName() {
return name;
}
@Override
public double getPrice() {
return price;
}
@Override
public void print() {
System.out.println(" " + getName() + ", " + getPrice());;
}
}
(4)創建女招待
/**
* 女招待
*/
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
print(allMenus);
}
private void print(MenuComponent menuComponent) {
menuComponent.print();
for (MenuComponent child : menuComponent.getChilds()) {
print(child);
}
}
}
(5)使用女招待列印菜單
public class Test {
public static void main(String[] args) {
// 所有菜單
MenuComponent allMenus = new Menu("ALL MENUS");
// 子菜單
MenuComponent pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU");
MenuComponent dinerMenu = new Menu("DINER MENU");
MenuComponent dessertMenu = new Menu("DESSERT MENU");
// 添加煎餅屋菜單及菜單項
allMenus.addChild(pancakeHouseMenu);
pancakeHouseMenu.addChild(new MenuItem("Regular Pancake Breakfast", 2.99));
pancakeHouseMenu.addChild(new MenuItem("Blueberry Pancakes", 3.49));
pancakeHouseMenu.addChild(new MenuItem("Waffles", 3.59));
// 添加餐廳菜單及菜單項
allMenus.addChild(dinerMenu);
dinerMenu.addChild(new MenuItem("BLT", 2.99));
dinerMenu.addChild(new MenuItem("Soup of the day", 3.29));
dinerMenu.addChild(new MenuItem("Hotdog", 3.05));
// 添加甜點菜單及菜單項
dinerMenu.addChild(dessertMenu);
dessertMenu.addChild(new MenuItem("Apple Pie", 1.59));
dessertMenu.addChild(new MenuItem("Cheesecake", 1.99));
dessertMenu.addChild(new MenuItem("Sorbet", 1.89));
// 使用女招待列印菜單
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}