《Head First设计模式》第九章(2)组合模式

组合模式

​ 基于前一篇迭代模式的案例进行需求更新,餐厅的菜单管理系统需要有煎饼屋菜单和披萨菜单。现在希望在披萨菜单中能够加上一份餐后甜点的子菜单。
在迭代模式中,披萨菜单是用数组维护的,我们需要让披萨菜单持有一份子菜单,但是不能真的把他赋值给菜单项数组,因为类型不同,所以不能这么做。
所以,需要重新实现煎饼屋菜单和披萨菜单了。事实是,我们已经到达了一个复杂级别,如果现在不重新设计,就无法容纳未来增加的菜单或子菜单的需求。我们需要一下改变:

  • 需要某种树形结构,可以容纳菜单、子菜单和菜单项;
  • 需要确定能够在每个菜单的各个项之间游走,而且至少像用迭代器一样方便;
  • 需要能够更有弹性地在菜单项之间游走。比方说,可能只需要遍历甜点菜单,或者可以便利整个菜单;

我们首先想到的是采用树形结构:

《Head First设计模式》第九章(2)组合模式_第1张图片

​ 我们要使用组合模式来解决这个问题,但并没有放弃迭代器模式,它仍然是解决方案中的一部分,然而管理菜单的问题已经到了一个迭代器无法解决的新维度。所以,我们将倒退几步,使用组合模式来解决。

​ 组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。使用组合结构,我们能把相同的操作应用在组合的个别对象上,换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

定义

组合模式允许将对象组合成属性结构来表现“整体/部分”层次结构,组合能让客户以一致的方式处理个别对象以及对象组合。

组合模式能创建一个树形结构

《Head First设计模式》第九章(2)组合模式_第2张图片

​ 《Head First设计模式》第九章(2)组合模式_第3张图片

我们要如何将组合模式利用在菜单上呢?一开始,我们需要创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用同意的做法来处理菜单和菜单项。来看看设计的类图:

《Head First设计模式》第九章(2)组合模式_第4张图片

​ 菜单组件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

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();

    }

}

 

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());

    }

}

 

public class Menu extends MenuComponent {

    ArrayList menuComponents = new ArrayList();

    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("----------------------");

 

        Iterator iterator = menuComponents.iterator();

        while(iterator.hasNext()) {

            MenuComponent menuComponent = iterator.next();

            menuComponent.print();

        }

    }

}

 

 

public class Waitress {

    MenuComponent allMenus;

 

    public Waitress(MenuComponent allMenus) {

        this.allMenus = allMenus;

    }

 

    public void printMenu() {

        allMenus.print();

    }

}

 

 

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("苹果煎饼""香甜苹果煎饼"true5.99));

        pizzaHouseMenu.add(new MenuItem("至尊披萨""意大利至尊咖啡"false12.89));

        cafeMenu.add(new MenuItem("美式咖啡""香浓美式咖啡"true3.89));

 

        Waitress waitress = new Waitress(allMenus);

        waitress.printMenu();

    }

 

}

《Head First设计模式》第九章(2)组合模式_第5张图片

​ 组合模式以单一责任设计原则换取透明性。通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点一视同仁。也就是说,一个元素究竟是组合还是叶节点,对客户是透明的。

​ 现在,我们在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

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;

    }

 

}

这个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

//跟所有的迭代器一样,我们实现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。我们等于创建了一个迭代器,其作用是“没作用”。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

class NullIterator implements Iterator{

 

    @Override

    public boolean hasNext() {

        return false;

    }

 

    @Override

    public Object next() {

        return null;

    }

 

    @Override

    public void remove() {

        throw  new UnsupportedOperationException();

    }

}

​ 以上便是组合模式的一些内容。

你可能感兴趣的:(杂记)