文章目录
组合(Composite)模式
隶属类别——对象结构型模式
1. 意图
将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
2. 别名
无
3. 动机
在绘图编辑器和图像捕捉系统这样的图形应用程序中,用户可以使用简单的组件创建图标。用户可以组合多个简单的组件以形成一些较大的组件,这些组件有可以组合成更大的组件,例如正常菜单中包含一个甜食菜单,甜食菜单中包含多个具体的甜食。一个简单的实现方法是为Text和Line这样的图元定义一些类,另外定义一些类作为这些图元的容器类(Container)。
然而这种方法存在一个问题,使用这些类的代码必须区别对待图元对象与容器对象,而实际上大多数情况下用户认为它们是一样的。对这些类区别使用,使得程序更加复杂。Composite模式描述了如何使用递归组合,使用的用户不必对这些类进行区别,如下图所示。
Composite模式的关键是一个抽象类,它既可以代表图元,又可以代表图元的容器。在图形系统中的这个类就是Graphic,它声明一些与特定图形的操作,例如draw,同时它也声明了所有的组合对象共享的一组操作,例如一些操作用于访问和管理它的子部件。
子类Line、Rectangle和Text(参见前面的类图)定义了一些图元对象,这些类实现分别用于绘制直线,矩形和正文。由于图元都没有子图形,因此他们都不执行与子类有关的操作。
Picture类定义了一个Graphic对象的聚合。Picture的draw操作是通过对它的子部件调用draw实现的,Picture还用这种国防法实现了一些与子部件相关的操作。由于Picture接口与Graphics接口是一致的,以此Picture对象可以递归的组合其他Picture对象。
4. 适用性
以下情况使用Composite模式
- 你想表示对象的部分-整体层次结构。
- 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
5. 结构
经典的Composite对象结构如下图所示:
6. 参与者
- Component(Graphics)
- 为组合中的对象声明接口。
- 在适当的情况下,实现所有类共用接口的缺省行为。
- 声明一个接口用于访问和管理Component的子组件。
- (可选的)在递归结构中声明一个接口,用于访问一个父部件,并在合适的情况下实现它。
- Leaf(Rectangle,Line,Text)
- 在组合中表示叶节点对象,叶节点没有子节点。
- 在和使用定义类似于图元对象的行为。
- Composite(Picture)
- 定义有子部件的那个那些部件的行为。
- 存储子部件。
- 实现Component接口中与子部件有关的操作。
- Client
- 通过Component接口操纵组合部件的对象。
7. 协作
- 用户使用Component类接口与组合结构中的对象进行交互。如果接收者是一个叶节点,则直接处理请求,执行operation操作。如果接收是Composite,它通常将请求发送给它的子部件,在转发请求之前/或之后可能执行一些辅助操作。例如进行一些简单的统计操作?试一下
8. 效果
优点
- 定义了包含基本对象和组合对象的类层次结构 基本对象可以被组合成更复杂的组合对象,而这个组合对象有可以被组合。这样不断的递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象。
- 简化客户代码 客户可以一直地使用组合结构和单个对象。通常用户不知道(也不关系)处理的是一个叶节点还是一个组合组件。这就简化了客户代码,因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。
- 使得更容易增加新类型的组件 新定义的Composite或者Leaf子类自动地与已有的客户代码一起工作,客户程序不需因新的Component类而改变。
缺点
- 使得你的设计变得更加一般化 容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有特定组件的组件。使用Composite时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。
9. 实现
我们在实现Composite模式时需要考虑以下几个问题:
-
-
显式的父部件引用 保持从子部件到父部件的引用能简化组合结构的遍历和管理。父部件引用可以简化结构的上移和组件的删除,同时父部件引用也支持Chain of Responsibility(责任链)模式。
通常在Component类中定义父部件引用。Leaf和Composite类可以继承这个引用以及管理这个引用的那些操作。
对于父部件引用,必须维护一个不变式,即一个组合的所有子节点以这个组合为父节点,而反之该组件以这些节点为子节点。保证这一点最容易的办法是,仅当在一个组合中增加或删除一个组件时,才能改变这个组件的父部件。如果能在Composite类的Add 和Remove操作中实现这种方法,那么的子类都可以继承这一方法,并且将自动维护这一不变式。
-
-
-
共享组件 共享组件时很有用的,比如它可以减少对存贮的需要。但是当一个组件只有一个父部件是,很难共享组件。
一个可行的解决办法是为子部件存贮多个父部件(Java中,一个子部件应该无法具有多个父部件吧),但当一个请求在结构中向上传递时,这种方法会导致多义性。Flyweight模式讨论了如何修改设计以避免将父部件存贮在一起的方法。如果子部件可以将一些状态(或是所有的状态)存储在外部,从而不需要向父部件发送请求,那么,这种方法是可行的。
-
-
-
最大化Component接口 Composite模式的目的是使得用户不知道他们正在使用的具体的Leaf和Composite类。为了达到这一目的,Composite类应为Leaf何Composite类尽可能多定义一些公共操作。Composite类通常为这些操作提供缺省操作,而Leaf和Composite子类可以对这些操作进行重定义。
然而,这个目标又是能回与类层次结构设计原则向冲突,该原则规定:一个类只能定义那些对它的子类有意义的操作。有许多Component所支持的操作对Leaf类似乎没有什么意义,那么Component怎么为它们提供一个缺省操作呢,在Java中可以通过抛出UnsupportedOperationException的方式,
有时一点创造性可以使得一个1看起来仅对Composite才有意义的操作,将它移入Component类中,就会对所有的Component都适用。例如,访问子节点的接口是Composite类的一个基本组成部分,但对Leaf
类来说不必要,但是如果我们把一个Leaf看成一个没有子节点的Component,就可以在Component类中定义一个缺省的操作,用于对子节点进行访问,这个缺省的操作不返回任何一个子节点。Leaf类可以使用缺省的实现,而Composite类则会重新实现这个操作以返回它们的子类。
管理子部件的操作比较复杂,我们将在下一项中予以讨论。
-
-
-
声明管理子部件的操作 虽然Composite类实现了Add和Remove操作用于管理子部件,但在Composite模式中一个重要的问题是:在Composite类层次结构中哪一些类声明这些操作。我们是应该在Component类声明这些操作,并使这些操作对Leaf类有意义呢,还是只应该在Composite和它的子类中声明并定义这些操作呢?
这需要在安全性和透明性之间做出权衡选择。
- 在类层次结构中的根部定义子节点管理接口的方法具有良好的透明性,因为你可以一致地使用所有的组件,但是这一方法是以安全性为代价的,因为客户有可能会做一些无意义的事情,例如在Leaf中增加和删除对象等
- 在Composite类中定义管理子部件的方法具有良好的安全性,在像C++这样的静态类型语言中,在编译时任何从Leaf中增加和删除对象的尝试都将被发现,但是这又损失了透明性,因为Leaf和Composite具有不同的接口。(Java中应该是在运行时才会发现吧)
在Composite模式中,相对于安全性,我们比较强调透视性。如果你选择了安全性,有时你可能会丢失类型信息,并且不得不将一个组件转化为了一个组合。这样的类型转化必定不是类型安全的。
一种办法是在Component类中声明一个操作Composite getComposite(). Component提供了一个返回空指针的缺省操作。 Component类重定义这个操作并通过this指针返回它自身。getComposite允许你查询一个组件看它是否是一个组合,你可以对返回的组合安全地执行add和remove操作。
提供透明性的唯一方法是在Component中定义缺省add和remove操作。这又带来了一个新的问题:Component.add()的实现不可避免地会有失败的可能性。你可以不让Component.add()不做如何事情,但这就忽略了一个很重要的问题,企图想叶节点中增加一些东西时可能会引入错误,但是add操作会产生垃圾。你可以让add操作删除它的参数,但可能客户并不希望这样。
如果该组件不允许有子部件,或者Remove的参数不是该组件的子节点时,通常最好使用缺省方式(可能是产生一个异常)处理add和remove的失败。
-
-
- Component是否应该实现一个Component列表 你可能希望在Component类中将子节点集合定义为一个实例变量,而这个Component类中也声明了一些操作对子节点进行访问和管理,但是在积累中存放子类指针,对叶节点来说会导致空间浪费,因为叶节点根本没有子节点,只有当该结构中子类数目相对较少而言,才值得使用这种方法。
-
-
子部件排序 许多设计制定了Composite的子部件顺序。在前面的Graphics例子中,排序可能表示了从前至后的顺序。如果Composite表示语法分析树,Composite子部件的顺序必须反映程序结构,而组合语句就是这样一些Composite的实例。
如果需要考虑子节点的顺序时,必须仔细地设计对子节点的访问和管理接口,一边管理子节点的顺序。Iterator模式在这个方面给予了一些指导。
-
-
-
使用高速缓冲存贮改善性能 如果你需要对组合进行频繁的遍历或查找,Composite类可以缓冲存储对它的子节点进行遍历和查找的相关信息。例如,动机一节的例子中的Picture类能高速存贮其子部件的边界框,在绘图或选择期间,当子部件在当前窗口不可见时,这个边界框使得Picture不需要再进行绘图或选择。
一个组件发生变化时,它的父部件原先缓冲存贮的信息也变得无效。在组件知道其父部件是,这个方法最为有效。因此,如果你使用高速缓冲存贮,你需要定义一个接口来通知组合组件它们所缓冲存贮的信息无效。(如何定义,如何设置)
-
-
- 应该由谁删除Component 在没有垃圾回收机制的语言中,当一个Composite被销毁时,通常最好有Composite负责删除其子节点。但有一种情况除外,即Leaf对象不会改变,因此可以被共享。
-
- 存贮组件最好用哪一种数据结构 Composite可使用多种数据结构存贮他们的子节点,包括连接列表,树,数组和hash表。数据结构的选择取决于肖略。事实上,使用通用数据结构根本没有必要。有时对每个子节点,Composite都有一个变量与之对应,这就要求Composite的每个子类都要实现自己的管理接口,参见Interpreter模式中例子。
10. 代码示例
首先是Component——MenuComponent.java
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
public Iterator<MenuComponent> createIterator() {
throw new UnsupportedOperationException();
}
}
接下来是Leaf——MenuItem.java
public class MenuItem extends MenuComponent {
private String name;
private String description;
private boolean vegetarian;
private double price;
public MenuItem(String name,
String description,
boolean vegetarian,
double price) {
this.name = name;
this.description = description;
this.vegetarian = vegetarian;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean isVegetarian() {
return vegetarian;
}
@Override
public double getPrice() {
return price;
}
@Override
public Iterator<MenuComponent> createIterator(){
return new NullIterator();
}
@Override
public void print() {
System.out.print(" " + getName());
if (isVegetarian()) {
System.out.print("(v)");
}
System.out.println("," + getPrice());
System.out.println(" --" + getDescription());
}
}
已经Composite——Menu.java
public class Menu extends MenuComponent {
List<MenuComponent> menuComponents = new ArrayList<>();
private String name;
private String description;
public Menu(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
@Override
public MenuComponent getChild(int i) {
return (MenuComponent)menuComponents.get(i);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public Iterator<MenuComponent> createIterator() {
return new CompositeIterator(menuComponents.iterator());
}
@Override
public void print() {
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("-----------");
Iterator<MenuComponent> iterator = menuComponents.iterator();
while (iterator.hasNext()) {
MenuComponent menuComponent =
(MenuComponent)iterator.next();
menuComponent.print(); // 利用多态性。
}
}
}
对应的两个Iterator——NullIterator.java & CompositeIterator.java
NullIterator.java
public class NullIterator implements Iterator<MenuComponent>{
@Override
public MenuComponent next() {
return null;
}
@Override
public boolean hasNext() {
return false;
}
}
CompositeIterator.java
public class CompositeIterator implements Iterator<MenuComponent> {
Stack<Iterator<MenuComponent>> stack = new Stack();
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
@Override
public MenuComponent next() {
if (hasNext()) {
Iterator<MenuComponent> iterator = (Iterator<MenuComponent>) stack.peek();
MenuComponent component = (MenuComponent) iterator.next();
if (component instanceof Menu) {
stack.push(component.createIterator());
}
return component;
} else {
return null;
}
}
@Override
public boolean hasNext() {
if (stack.empty()) {
return false;
} else {
Iterator<MenuComponent> iterator = (Iterator<MenuComponent>) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
return true;
}
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
和Client——Waitress.java
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
public void printVegetarianMenu() {
Iterator<MenuComponent> iterator = allMenus.createIterator();
System.out.println("\nVEGETARIAN MENU\n----");
while (iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
try {
if (menuComponent.isVegetarian()) {
menuComponent.print();
}
} catch (UnsupportedOperationException e) {}
}
}
}
Now,已经最后的测试类——MenuTestDrive.java
public class MenuTestDrive {
public static void main(String[] args) {
// TODO Auto-generated method stub
MenuComponent pancakeHouseMenu =
new Menu("PANCAKE HOUSE MENU" , "Breakfast");
MenuComponent dinerMenu =
new Menu("DINER MENU", "Lunch");
MenuComponent cafeMenu =
new Menu("CAFE MENU", "Dinner");
MenuComponent dessertMenu =
new Menu("DESSERT MENU", "Dessert of course");
MenuComponent coffeeMenu = new Menu("COFFEE MENU", "Stuff to go with your afternoon coffee");
MenuComponent allMenus = new Menu("ALL MENUS", "ALL menus combined");
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
pancakeHouseMenu.add(new MenuItem(
"K&B's Pancake Breakfast",
"Pancakes with scrambled eggs, and toast",
true,
2.99));
pancakeHouseMenu.add(new MenuItem(
"Regular Pancake Breakfast",
"Pancakes with fried eggs, sausage",
false,
2.99));
pancakeHouseMenu.add(new MenuItem(
"Blueberry Pancakes",
"Pancakes made with fresh blueberries, and blueberry syrup",
true,
3.49));
pancakeHouseMenu.add(new MenuItem(
"Waffles",
"Waffles, with your choice of blueberries or strawberries",
true,
3.59));
dinerMenu.add(new MenuItem(
"Vegetarian BLT",
"(Fakin') Bacon with lettuce & tomato on whole wheat",
true,
2.99));
dinerMenu.add(new MenuItem(
"BLT",
"Bacon with lettuce & tomato on whole wheat",
false,
2.99));
dinerMenu.add(new MenuItem(
"Soup of the day",
"A bowl of the soup of the day, with a side of potato salad",
false,
3.29));
dinerMenu.add(new MenuItem(
"Hotdog",
"A hot dog, with saurkraut, relish, onions, topped with cheese",
false,
3.05));
dinerMenu.add(new MenuItem(
"Steamed Veggies and Brown Rice",
"Steamed vegetables over brown rice",
true,
3.99));
dinerMenu.add(new MenuItem(
"Pasta",
"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
true,
3.89));
dinerMenu.add(dessertMenu);
dessertMenu.add(new MenuItem(
"Apple Pie",
"Apple pie with a flakey crust, topped with vanilla icecream",
true,
1.59));
dessertMenu.add(new MenuItem(
"Cheesecake",
"Creamy New York cheesecake, with a chocolate graham crust",
true,
1.99));
dessertMenu.add(new MenuItem(
"Sorbet",
"A scoop of raspberry and a scoop of lime",
true,
1.89));
cafeMenu.add(new MenuItem(
"Veggie Burger and Air Fries",
"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
true,
3.99));
cafeMenu.add(new MenuItem(
"Soup of the day",
"A cup of the soup of the day, with a side salad",
false,
3.69));
cafeMenu.add(new MenuItem(
"Burrito",
"A large burrito, with whole pinto beans, salsa, guacamole",
true,
4.29));
cafeMenu.add(coffeeMenu);
coffeeMenu.add(new MenuItem(
"Coffee Cake",
"Crumbly cake topped with cinnamon and walnuts",
true,
1.59));
coffeeMenu.add(new MenuItem(
"Bagel",
"Flavors include sesame, poppyseed, cinnamon raisin, pumpkin",
false,
0.69));
coffeeMenu.add(new MenuItem(
"Biscotti",
"Three almond or hazelnut biscotti cookies",
true,
0.89));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
waitress.printVegetarianMenu();
}
}
以及对应的测试结果
ALL MENUS, ALL menus combined
-----------
PANCAKE HOUSE MENU, Breakfast
-----------
K&B's Pancake Breakfast(v),2.99
--Pancakes with scrambled eggs, and toast
Regular Pancake Breakfast,2.99
--Pancakes with fried eggs, sausage
Blueberry Pancakes(v),3.49
--Pancakes made with fresh blueberries, and blueberry syrup
Waffles(v),3.59
--Waffles, with your choice of blueberries or strawberries
DINER MENU, Lunch
-----------
Vegetarian BLT(v),2.99
--(Fakin') Bacon with lettuce & tomato on whole wheat
BLT,2.99
--Bacon with lettuce & tomato on whole wheat
Soup of the day,3.29
--A bowl of the soup of the day, with a side of potato salad
Hotdog,3.05
--A hot dog, with saurkraut, relish, onions, topped with cheese
Steamed Veggies and Brown Rice(v),3.99
--Steamed vegetables over brown rice
Pasta(v),3.89
--Spaghetti with Marinara Sauce, and a slice of sourdough bread
DESSERT MENU, Dessert of course
-----------
Apple Pie(v),1.59
--Apple pie with a flakey crust, topped with vanilla icecream
Cheesecake(v),1.99
--Creamy New York cheesecake, with a chocolate graham crust
Sorbet(v),1.89
--A scoop of raspberry and a scoop of lime
CAFE MENU, Dinner
-----------
Veggie Burger and Air Fries(v),3.99
--Veggie burger on a whole wheat bun, lettuce, tomato, and fries
Soup of the day,3.69
--A cup of the soup of the day, with a side salad
Burrito(v),4.29
--A large burrito, with whole pinto beans, salsa, guacamole
COFFEE MENU, Stuff to go with your afternoon coffee
-----------
Coffee Cake(v),1.59
--Crumbly cake topped with cinnamon and walnuts
Bagel,0.69
--Flavors include sesame, poppyseed, cinnamon raisin, pumpkin
Biscotti(v),0.89
--Three almond or hazelnut biscotti cookies
VEGETARIAN MENU
----
K&B's Pancake Breakfast(v),2.99
--Pancakes with scrambled eggs, and toast
Blueberry Pancakes(v),3.49
--Pancakes made with fresh blueberries, and blueberry syrup
Waffles(v),3.59
--Waffles, with your choice of blueberries or strawberries
Vegetarian BLT(v),2.99
--(Fakin') Bacon with lettuce & tomato on whole wheat
Steamed Veggies and Brown Rice(v),3.99
--Steamed vegetables over brown rice
Pasta(v),3.89
--Spaghetti with Marinara Sauce, and a slice of sourdough bread
Apple Pie(v),1.59
--Apple pie with a flakey crust, topped with vanilla icecream
Cheesecake(v),1.99
--Creamy New York cheesecake, with a chocolate graham crust
Sorbet(v),1.89
--A scoop of raspberry and a scoop of lime
Apple Pie(v),1.59
--Apple pie with a flakey crust, topped with vanilla icecream
Cheesecake(v),1.99
--Creamy New York cheesecake, with a chocolate graham crust
Sorbet(v),1.89
--A scoop of raspberry and a scoop of lime
Veggie Burger and Air Fries(v),3.99
--Veggie burger on a whole wheat bun, lettuce, tomato, and fries
Burrito(v),4.29
--A large burrito, with whole pinto beans, salsa, guacamole
Coffee Cake(v),1.59
--Crumbly cake topped with cinnamon and walnuts
Biscotti(v),0.89
--Three almond or hazelnut biscotti cookies
Coffee Cake(v),1.59
--Crumbly cake topped with cinnamon and walnuts
Biscotti(v),0.89
--Three almond or hazelnut biscotti cookies
这里放一下UML类图:
11. 已知应用
几乎所有面向对象的系统中都有Composite模式的应用实例。在Smalltalk中的Model/View/Controller结构中,原始View类就是一个Composite,几乎每个用户界面工具箱货框架都遵循这些步骤。
RTL Smalltalk编译器框架大量地使用了Composite模式。RTLExxpression是一个对应于语法分析树的Component类。它有一些子类,例如BinaryExpression,而BinaryExpression包含了子RTLExpression对象。这些类为语法分析树定义了一个组合结构。RegisterTransfer是一个用于程序的中间Single Static Assignment(SSA)形式的Component类。RegisterTransfer的Leaf子类定义了一些不同的静态赋值形式,例如:
- 基本赋值,在两个寄存器上执行操作并且将结果放入第三个寄存器。
- 具有源寄存器但无目标寄存器的辅助,这说明是在例程返回后使用该寄存器。
- 具有目标寄存器但无源寄存器的赋值,这说明了在例程开始之前分配目标寄存器。
另一个子类RegisterTransferSet,是一个Composite类,表示一次改变几个寄存器的赋值。
这种模式的另外一个例子出现在财经领域,在这一个领域中,一个资产组合聚合多个单个资产,为了支持赋值的资产聚合,资产组合可以用一个Composite类实现。这个Composite类与单个资产的接口一致。
Command模式藐视的MacroCommand其实也可以用到了组合设计模式,一个MacroCommand中可以包含多个command,这些command部分有可能是MacroCommand,并以一定顺序排列。
12. 相关模式
- 责任链模式:通常部件—父部件连接用于Responsibility of Chain模式。
- 装饰器模式:Decorator模式经常和Composite模式一起使用。当装饰和组合一起使用时,他们通常有一个公共的父类。因此装饰器必须支持具有add、remove和getChild操作的Component接口。
- 享元模式: flyweight让你分享组件,但不再能引用他们的父部件
- 迭代器模式:Iterator模式可以用来遍历Composite
- 访问者模式: Visitor将本来应该分布在Composite和Leaf类中的操作和行为局部化。
13. 设计原则口袋
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不要针对实现编程
- 为交互对象之间的松耦合设计而努力
- 类应该对扩展开放,对修改关闭
- 依赖抽象,不要依赖具体类
- 只和密友交谈
- 好莱坞原则——别找我,我会找你
- 单一责任原则——类应该只有一个改变的理由。
14. 参考文献
《设计模式:可复用面向对象软件的基础》
《HeadFirst设计模式》