设计模式之组合模式
作者:互联网
需求分析
餐厅的菜单管理系统需要有煎饼屋菜单和披萨菜单。现在希望在披萨菜单中能够加上一份餐后甜点的子菜单。
我们需要一下改变:
- 需要某种树形结构,可以容纳菜单、子菜单和菜单项;
- 需要确定能够在每个菜单的各个项之间游走,而且至少像用迭代器一样方便;
- 需要能够更有弹性地在菜单项之间游走。比方说,可能只需要遍历甜点菜单,或者可以便利整个菜单;
我们首先想到的是采用树形结构:
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。使用组合结构,我们能把相同的操作应用在组合的个别对象上,换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。
组合模式定义
组合模式允许将对象组合成属性结构来表现“整体/部分”层次结构,组合能让客户以一致的方式处理个别对象以及对象组合。
组合模式能创建一个树形结构
组合模式类图
注:组件、组合、树? 组合包含组件。组件有两种:组合与叶节点元素。听起来象递归是不是? 组合持有一群孩子,这孩子可以是别的组合或者叶节点元素。
利用组合设计菜单
设计思路
我们需要创建一个组件接口MenuComponent来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。来看看设计的类图:
菜单组件MenuComponent提供了一个接口,让菜单项和菜单共同使用。因为我们希望能够为这些方法提供默认的实现,所以我们在这里可以把MenuComponent接口换成一个抽象类。
在这个类中,有显示菜单信息的方法getName()等,还有操纵组件的方法add(), remove(), getChild()等。菜单项MenuItem覆盖了显示菜单信息的方法,而菜单Menu覆盖了一些对他有意义的方法。
代码实现
1. 实现菜单组件
所有的组件都必须实现MenuComponent接口;然而,叶节点和组合节点的角色不同,所以有些方法
可能并不适合某种节点。面对这种情况,有时候,你最好是抛出运行时异常。
public abstract class MenuComponent {
// add,remove,getchild
// 把组合方法组织在一起,即新增、删除和取得菜单组件
public void add(MenuComponent component) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent component) {
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();
}
}
2. 实现菜单项类。这是组合类图里的叶类,它实现组合内元素的行为。
public class MenuItem extends MenuComponent {
String name;
String description;
boolean vegetarian;
double price;
public MenuItem(String name, String description, boolean vegetarian, double price) {
this.name = name;
this.description = description;
this.vegetarian = vegetarian;
this.price = price;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public boolean isVegetarian() {
return vegetarian;
}
public double getPrice() {
return price;
}
public void print() {
System.out.println(" " + getName());
if (isVegetarian()) {
System.out.println("(V)");
}
System.out.println(", " + getPrice());
System.out.println(" -- " + getDescription());
}
}
3. 实现组含菜单
public class Menu extends MenuComponent {
// 菜单可以有任意数的孩子,都必须属子MenuComponent类型,使用ArrayList记录它们。
ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
String name;
String description;
public Menu(String name, String description) {
this.name = name;
this.description = description;
}
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public void print() {
System.out.println("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("----------------------");
// 在遍历期间,如果遇到另一个菜单对象,它的print()方法会开始另一个遍历,依次类推。
Iterator<MenuComponent> iterator = menuComponents.iterator();
while(iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
menuComponent.print();
}
}
}
4. 更新女招待的代码
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
5. 编写测试程序
public class Client {
public static void main(String[] args) {
// 创建菜单对象
MenuComponent pancakeHouseMenu = new Menu("煎饼屋菜单", "提供各种煎饼。");
MenuComponent pizzaHouseMenu = new Menu("披萨屋菜单", "提供各种披萨。");
MenuComponent cafeMenu = new Menu("咖啡屋菜单", "提供各种咖啡");
// 创建一个顶层的菜单
MenuComponent allMenus = new Menu("All Menus", "All menus combined");
// 把所有菜单都添加到顶层菜单
allMenus.add(pancakeHouseMenu);
allMenus.add(pizzaHouseMenu);
allMenus.add(cafeMenu);
// 在这里加入菜单项
pancakeHouseMenu.add(new MenuItem("苹果煎饼", "香甜苹果煎饼", true, 5.99));
pizzaHouseMenu.add(new MenuItem("至尊披萨", "意大利至尊咖啡", false, 12.89));
cafeMenu.add(new MenuItem("美式咖啡", "香浓美式咖啡", true, 3.89));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
在运行时菜单组合是什么样的:
组合模式以单一责任设计原则换取透明性。通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点一视同仁。也就是说,一个元素究竟是组合还是叶节点,对客户是透明的。
现在,我们在MenuComponent类中同时具有两种类型的操作。因为客户有机会对一个元素做一些不恰当或是没有意义的操作,所以我们失去了一些安全性。
组合迭代器
我们现在再扩展一下,这种组合菜单如何设计迭代器呢?细心的朋友应该观察到,我们刚才使用的迭代都是递归调用的菜单项和菜单内部迭代的方式。现在我们想设计一个外部迭代的方式怎么办?譬如出现一个新需求:服务员需要打印出蔬菜性质的所有食品菜单。
首先,我们给MenuComponent加上判断蔬菜类食品的方法,然后在菜单项中进行重写:
public abstract class MenuComponent {
…………
/**
* 判断是否为蔬菜类食品
*/
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
}
/**
* 菜单项
*/
public class MenuItem extends MenuComponent{
String name;
double price;
/**蔬菜类食品标志*/
boolean vegetarian;
…………
public boolean isVegetarian() {
return vegetarian;
}
public void setVegetarian(boolean vegetarian) {
this.vegetarian = vegetarian;
}
}
这个CompositeIterator是一个不可小觑的迭代器,它的工作是遍历组件内的菜单项,而且确保所有的子菜单(以及子子菜单……)都被包括进来。
//跟所有的迭代器一样,我们实现Iterator接口。
class CompositeIterator implements Iterator {
Stack stack = new Stack();
/**
*将我们要遍历的顶层组合的迭代器传入,我们把它抛进一个堆栈数据结构中
*/
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
@Override
public boolean hasNext() {
//想要知道是否还有下一个元素,我们检查堆栈是否被清空,如果已经空了,就表示没有下一个元素了
if (stack.empty()) {
return false;
} else {
/**
*否则我们就从堆栈的顶层中取出迭代器,看看是否还有下一个元素,
*如果它没有元素,我们将它弹出堆栈,然后递归调用hasNext()。
*/
Iterator iterator = (Iterator) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
//否则,便是还有下一个元素
return true;
}
}
}
@Override
public Object next() {
//好了,当客户想要取得下一个元素时候,我们先调用hasNext()来确定时候还有下一个。
if (hasNext()) {
//如果还有下一个元素,我们就从堆栈中取出目前的迭代器,然后取得它的下一个元素
Iterator iterator = (Iterator) stack.peek();
MenuComponent component = (MenuComponent) iterator.next();
/**
*如果元素是一个菜单,我们有了另一个需要被包含进遍历中的组合,
*所以我们将它丢进对战中,不管是不是菜单,我们都返回该组件。
*/
if (component instanceof Menu) {
stack.push(component.createIterator());
}
return component;
} else {
return null;
}
}
@Override
public void remove() {
// 我们不支持删除,这里只有遍历
throw new UnsupportedOperationException();
}
}
在我们写MenuComponent类的print方法的时候,我们利用了一个迭代器遍历组件内的每个项,如果遇到的是菜单,我们就会递归地调用print()方法处理它,换句话说,MenuComponent是在“内部”自行处理遍历。
但是在上页的代码中,我们实现的是一个“外部”的迭代器,所以有许多需要追踪的事情。外部迭代器必须维护它在遍历中的位置,以便外部客户可以通过hasNext()和next()来驱动遍历。在这个例子中,我们的代码也必须维护组合递归结构的位置,这也就是为什么当我们在组合层次结构中上上下下时,使用堆栈来维护我们的位置。
空迭代器
菜单项没什么可以遍历的,那么我们要如何实现菜单项的createIterator()方法呢。
- 1:返回null。我们可以让createIterator()方法返回null,但是如果这么做,我们的客户代码就需要条件语句来判断返回值是否为null;
- 2:返回一个迭代器,而这个迭代器的hasNext()永远返回false。这个是更好的方案,客户不用再担心返回值是否为null。我们等于创建了一个迭代器,其作用是“没作用”。
class NullIterator implements Iterator{
@Override
public boolean hasNext() {
// 当hasNext调用时,永远返回false
return false;
}
@Override
public Object next() {
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
给我素食菜单
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 {
// 判断素食,菜单会抛异常,捕获了就能正常的遍历菜单项
// 虽说可以 instanceof 进行运行时的类型检查,但是这样就会失去菜单和菜单项的透明性,我们只需要关注他们的接口就行
// 或者可以选择让菜单的 isVegetarian() 返回 false,这样也能保证程序的透明性
if (menuComponent.isVegetarian()) {
menuComponent.print();
}
} catch (UnsupportedOperationException e) { }
// 只能调用菜单项的 print(),不能调用菜单的 print()。因为这里是靠迭代器实现的,如果调用菜单的 print() 会重复打印
}
}
}
使用场景:当你有数个对象的集合,它们彼此之间有“整体/部分”的关系,并且你想用一致的方式对待这些对象时,你就需要使用组合模式。
组合使用的结构:通常是用树形结构,也就是一种层次结构。根就是顶层的组合,然后往下是它的孩子,最末端是叶节点。
优点:我认为我让客户生活得更加简单。我的客户不再需要操心面对的是组合对象还是叶节点对象了,所以就不需要写一大堆if语句来保证他们对正确的对象调用了正确的方法。通常,他们只需要对整个结构调用一个方法并执行操作就可以了.
要点
- 组合模式提供一个结构,可同时包容个别对象和组合对象。
- 组合模式允许客户对个别对象以及组合对象一视同仁。
- 组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点。在实现组合模式时,有许多设计上的折衷。你要根据需要平衡透明性和安全性。
标签:菜单,组合,MenuComponent,模式,菜单项,new,设计模式,public 来源: https://www.cnblogs.com/pursuingdreams/p/15721067.html