bboyjing's blog

HeadFirst设计模式十一【组合模式】

本章节将学组合模式,来解决上一节遗留的问题。这个模式能够创建一个树形结构,在同一个结构中处理嵌套菜单和菜单项组。场景在上一章已经描述清楚了,下面就直接来实现。

代码示例

  1. 创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。该接口提供一些默认方法,默认行为都是抛出不支持异常,菜单和菜单项针对各自的情况进行重写。

    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
    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();
    }
    }
  2. 实现菜单项

    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
    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;
    }
    @Override
    public String getName() {
    return name;
    }
    @Override
    public String getDescription() {
    return description;
    }
    @Override
    public double getPrice() {
    return price;
    }
    @Override
    public boolean isVegetarian() {
    return vegetarian;
    }
    @Override
    public void print() {
    System.out.print(" " + getName());
    if (vegetarian) {
    System.out.print("(v)");
    }
    System.out.println(", " + getPrice());
    System.out.println(" -- " + getDescription());
    }
    }
  3. 实现组合菜单,这里的menueComponents是关键,其中可以存放MenuItem或者Menu本身,这里的print()会递归打印所有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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public class Menu extends MenuComponent {
    ArrayList<MenuComponent> menueComponents = new ArrayList();
    String name;
    String description;
    public Menu(String name, String description) {
    this.name = name;
    this.description = description;
    }
    @Override
    public void add(MenuComponent menuComponent) {
    menueComponents.add(menuComponent);
    }
    @Override
    public void remove(MenuComponent menuComponent) {
    menueComponents.remove(menuComponent);
    }
    @Override
    public MenuComponent getChild(int i) {
    return menueComponents.get(i);
    }
    @Override
    public String getName() {
    return name;
    }
    @Override
    public String getDescription() {
    return description;
    }
    @Override
    public void print() {
    System.out.print("\n" + getName());
    System.out.println(", " + getDescription());
    System.out.println("-----------------------");
    Iterator<MenuComponent> iterator = menueComponents.iterator();
    while (iterator.hasNext()) {
    MenuComponent menuComponent = iterator.next();
    menuComponent.print();
    }
    }
    }

这个模式在统一的接口中定义了两种实现需要的方法,然而两类方法又完全不相干,通过默认抛出不支持异常的方式来规避误操作,感觉有点不够干净。书上是这么说的,组合模式以单一责任设计原则换取透明性。什么是透明性?通过让组件的接口同时包含一些管理子节点和叶节点的操作,客户就可以将组合和叶节点一视同仁。也就是说一个元素究竟是组合还是叶节点对客户是透明的。如果说设计成多个不同接口的实现方式,客户的代码将必须用条件语句和instanceof操作符处理不同类型的节点。所以这是一个折衷的案例,还是有它存在的意义的。

组合迭代器

其实在print()方法中已经体现出迭代器了,但是如果我们需要使用迭代器来遍历整个组合的话,还是需要提供额外的功能。下面就来看下,怎么把迭代器组合加进来。

  1. MenuComponent接口添加createIterator()抽象方法。

    1
    2
    3
    4
    public abstract class MenuComponent {
    ......
    public abstract Iterator<MenuComponent> createIterator();
    }
  1. 创建一个空的迭代器,供MenuItem使用,因为该类中没有需要迭代的集合。Java8开始Iterator.remove()默认抛出不支持异常,所以这里不需要重写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class NullIterator implements Iterator<MenuComponent> {
    @Override
    public boolean hasNext() {
    return false;
    }
    @Override
    public MenuComponent next() {
    return null;
    }
    }
  2. 创建一个迭代器,供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
    public class CompositeIterator implements Iterator<MenuComponent> {
    /**
    * 保存Iterator的栈
    */
    Stack<Iterator<MenuComponent>> stack = new Stack<>();
    public CompositeIterator(Iterator<MenuComponent> iterator) {
    stack.push(iterator);
    }
    @Override
    public boolean hasNext() {
    if (stack.empty()) {
    return false;
    } else {
    // 查看栈顶迭代器
    Iterator<MenuComponent> iterator = stack.peek();
    if (!iterator.hasNext()) {
    // 如果栈顶迭代器中已经没有元素,则弹出
    stack.pop();
    // 继续查看栈顶
    return hasNext();
    } else {
    return true;
    }
    }
    }
    @Override
    public MenuComponent next() {
    if (hasNext()) {
    // 查看栈顶迭代器
    Iterator<MenuComponent> iterator = stack.peek();
    // 获取当前迭代器中的下一个元素
    MenuComponent menuComponent = iterator.next();
    if (menuComponent instanceof Menu) {
    // 栈顶迭代器中的下一个元素是菜单,则将菜单的Iterator入栈
    stack.push(((Menu) menuComponent).createIterator());
    }
    return menuComponent;
    } else {
    return null;
    }
    }
    }
  3. MenuItem实现createIterator()方法,其他代码不变,仅增加createIterator()方法

    1
    2
    3
    4
    5
    6
    public class Menu extends MenuComponent {
    @Override
    public Iterator<MenuComponent> createIterator() {
    return new CompositeIterator(menueComponents.iterator());
    }
    }
  4. Menu实现createIterator()方法,其他代码不变,仅增加createIterator()方法

    1
    2
    3
    4
    5
    6
    7
    public class MenuItem extends MenuComponent {
    ......
    @Override
    public Iterator<MenuComponent> createIterator() {
    return new NullIterator();
    }
    }
  5. 测试,构造的数据和上面一样,下面仅贴出迭代器使用部分代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Test {
    public static void main(String[] args) {
    ......
    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) {
    }
    }
    }
    }

组合模式就学到这里了。我们最后来复习下之前学过的几种设计模式的描述:

  • 策略模式:封装可互换的行为,并使用委托决定使用哪一个
  • 适配器:改变一个或多个类的接口
  • 迭代器:提供一个方式来遍历集合,而无需暴露集合的实现
  • 外观:简化一群类的接口
  • 组合:客户可以将对象的集合以及个别的对象一视同仁
  • 观察者:当某个状态改变时,允许一群对象能被通知到