组合模式
基于前一篇迭代模式的案例进行需求更新,餐厅的菜单管理系统需要有煎饼屋菜单和披萨菜单。现在希望在披萨菜单中能够加上一份餐后甜点的子菜单。
在迭代模式中,披萨菜单是用数组维护的,我们需要让披萨菜单持有一份子菜单,但是不能真的把他赋值给菜单项数组,因为类型不同,所以不能这么做。
所以,需要重新实现煎饼屋菜单和披萨菜单了。事实是,我们已经到达了一个复杂级别,如果现在不重新设计,就无法容纳未来增加的菜单或子菜单的需求。我们需要一下改变:
- 需要某种树形结构,可以容纳菜单、子菜单和菜单项;
- 需要确定能够在每个菜单的各个项之间游走,而且至少像用迭代器一样方便;
- 需要能够更有弹性地在菜单项之间游走。比方说,可能只需要遍历甜点菜单,或者可以便利整个菜单;
我们首先想到的是采用树形结构:
我们要使用组合模式来解决这个问题,但并没有放弃迭代器模式,它仍然是解决方案中的一部分,然而管理菜单的问题已经到了一个迭代器无法解决的新维度。所以,我们将倒退几步,使用组合模式来解决。
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。使用组合结构,我们能把相同的操作应用在组合的个别对象上,换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。
定义
组合模式允许将对象组合成属性结构来表现“整体/部分”层次结构,组合能让客户以一致的方式处理个别对象以及对象组合。
组合模式能创建一个树形结构
我们要如何将组合模式利用在菜单上呢?一开始,我们需要创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用同意的做法来处理菜单和菜单项。来看看设计的类图:
菜单组件MenuComponent提供了一个接口,让菜单项和菜单共同使用。因为我们希望能够为这些方法提供默认的实现,所以我们在这里可以把MenuComponent接口换成一个抽象类。在这个类中,有显示菜单信息的方法getName()等,还有操纵组件的方法add(),remove(),getChild()等。
菜单项MenuItem覆盖了显示菜单信息的方法,而菜单Menu覆盖了一些对他有意义的方法。
具体来看看代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
|
组合模式以单一责任设计原则换取透明性。通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点一视同仁。也就是说,一个元素究竟是组合还是叶节点,对客户是透明的。
现在,我们在MenuComponent类中同时具有两种类型的操作。因为客户有机会对一个元素做一些不恰当或是没有意义的操作,所以我们失去了一些安全性。
扩展:组合迭代器
我们现在再扩展一下,这种组合菜单如何设计迭代器呢?细心的朋友应该观察到,我们刚才使用的迭代都是递归调用的菜单项和菜单内部迭代的方式。现在我们想设计一个外部迭代的方式怎么办?譬如出现一个新需求:服务员需要打印出蔬菜性质的所有食品菜单。首先,我们给MenuComponent加上判断蔬菜类食品的方法,然后在菜单项中进行重写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
这个CmpositeIterator是一个不可小觑的迭代器,它的工作是遍历组件内的菜单项,而且确保所有的子菜单(以及子子菜单……)都被包括进来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
在我们写MenuComponent类的print方法的时候,我们利用了一个迭代器遍历组件内的每个项,如果遇到的是菜单,我们就会递归地电泳print方法处理它,换句话说,MenuComponent是在“内部”自行处理遍历。
但是在上页的代码中,我们实现的是一个“外部”的迭代器,所以有许多需要追踪的事情。外部迭代器必须维护它在遍历中的位置,以便外部可和可以通过hasNext和next来驱动遍历。在这个例子中,我们的代码也必须维护组合递归结构的位置,这也就是为什么当我们在组合层次结构中上上下下时,使用堆栈来维护我们的位置。
空迭代器
菜单项没什么可以遍历的,那么我们要如何实现菜单项的createIterator()方法呢。
1:返回null。我们可以让createIterator()方法返回null,但是如果这么做,我们的客户代码就需要条件语句来判断返回值是否为null;
2:返回一个迭代器,而这个迭代器的hasNext()永远返回false。这个是更好的方案,客户不用再担心返回值是否为null。我们等于创建了一个迭代器,其作用是“没作用”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
以上便是组合模式的一些内容。