# 迭代器 & 组合模式

# 现实案例

  • 现在有两个餐馆。他们各自实现了自己的餐点类;
  • 虽然两个人用了相同的菜单项类 MenuItem (单个菜的类);
  • 但是两个人用了不同的方式储存各自的菜单项;
    • A 用了 ArrayList();
    • B 用了数组;

2020-2-23-18-28-35.png

2020-2-23-18-27-27.png

  • 如果想要同时打印出两个菜单的所有菜单项,就要实现两种不同的遍历方式;
  • 如果又有其他店家采用了新的储存方式,我们就有要新加一种遍历方式;

2020-2-23-18-30-46.png

2020-2-23-18-31-12.png

# 上面实现的问题

  • 遍历代码是针对两个菜单类的具体实现来编码的,而不是针对接口;
  • 遍历代码需要知道每个菜单类内部是如何储存菜单项集合的,这违反了封装;
  • 违反了开放-封闭原则,如果另一种储存方式的菜单类出现,我们就要改遍历代码;
  • 有重复代码,上面用来两个 for-loop;

# 迭代器模式

  • 迭代器模式提供一种方法顺序访问一个聚合对象中的每个元素,而又不暴露其内部的实现
  • 我们需要创造一个迭代接口,接口中定义了用于遍历的方法;
  • 之后再定义迭代器类,让它们去针对不同的集合类型,去实现迭代接口;
  • 通过迭代模式,客户使用一种接口,透过迭代器对象就可以遍历不同类型的集合;

2020-2-23-18-57-29.png

# 用迭代器改写案例

  • 定义迭代接口;
public interface Iterator {
  boolean hasNext(); // 返回布尔值,判断是否后面还有元素
  Object next(); // 返回下一个元素
}
  • 分别实现各自的迭代类;

2020-2-23-18-47-55.png

  • 改写菜单类,增加方法 createIterator 去创建迭代器实例并返回;

2020-2-23-18-51-15.png

  • 客户去使用迭代器进行遍历;

2020-2-23-18-50-37.png

  • 上面的代码 Waitress 类和两个菜单类耦合在了一起;
  • 因为两个菜单类都有相同的 createIterator 方法,所以我们可以抽象出一个 Menu 接口;
  • 这样让 Waitress 类针对 Menu 接口编程,就可以降低耦合性了;

2020-2-23-18-56-40.png

2020-2-23-18-56-54.png

# 单一职责

  • 上面的例子中,菜单类只负责储存菜单项集合,并不符合遍历集合;
  • 如果我们让菜单类,即储存集合,又负责遍历集合;
  • 那么我们就给了他两个变化的原因。集合储存方式改变时,我们要改变菜单类,遍历方式改变时,菜单类也要改变;
  • 单一职责一个类应该只有一个引起变化的原因
  • 当类的变化原因增多时,出现 BUG 的几率也增加;
  • 当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有 “高内聚”;
  • 内聚用来度量一个类或模块,紧密地达成单一职责的程度;

  • 🌰 下面 👇 的 Game 类就可以拆分成另外三个类:

2020-2-23-19-7-23.png

# 现实案例

  • 现在菜单出现了新的变化;
  • 菜单之间可以嵌套;
  • 我们需要能够在遍历所有的菜单上的菜单项;
  • 也需要能够遍历指定的某个菜单;

2020-2-23-19-39-24.png

# 组合模式

  • 组合模式:允许你将对象组合成树形结构,来表现 “整体/部分” 的层次结构。组合能让客户已一致的方式处理个别对象以及组合对象;
  • 组合模式让我们能够用树形方式创建对象的结构,树里面包含了组合以及个别的对象;
  • 使用组合结构,我们能把相同的操作应用在组合和个别对象上;
  • 也就是,在大多数情况下,我们可以 “忽略” 对象组合和个别对象之间的差别;

# 用组合模式设计菜单

2020-2-23-20-47-55.png

  • 先来实现菜单组件 MenuComponent;
  • 菜单组件提供了一个接口,让菜单项(叶节点)和菜单(组合节点)共同使用;
  • 菜单组价是个抽象类,里面定义了一堆接口,有的是给菜单项用的,有的是给菜单用的;
  • 在菜单组件中,让方法默认抛出一个异常,这样如果菜单项或菜单不支持这个操作,他们不同做任何事情,直接继承实现就行了,默认就是抛出异常;

2020-2-23-20-51-0.png

  • 实现菜单项,在树结构中是 “叶节点”;

2020-2-23-20-54-14.png

  • 实现菜单。在树结构中是 “组合节点”;

2020-2-23-20-55-49.png

  • 注意菜单类的 print 方法的实现;
  • 因为菜单是一个组合,其中包括菜单项和其他的菜单;
  • 为了打印出它内部包含的所有菜品,我们采用递归的形式来实现;
    • 先用迭代器来遍历集合中的每一项;
    • 如果是菜单项就直接调用它的 print 方法;
    • 如果是其他菜单,就调用它的 print 方法,进入一个新的遍历;
    • 直到最后所有的项都遍历完,打印结束;

2020-2-23-20-57-19.png

# 迭代器 + 组合模式

  • 在 👆 上面的例子中,我们实现了菜单类的 print 方法。可以递归地打印出所有的菜品;
  • 但是整个遍历都是自动的,我们不能再此过程中追踪每一个菜品;
  • 我们希望能让客户可以灵活地控制遍历的每一步;
  • 🌰 例如,服务员希望游走整个菜单,然后挑出所有的素菜;
  • 通过 “迭代器 + 组合模式” 的形式,我们可以实现这个功能;

# 具体实现

  • 之前的菜单组件 MenuComponent 添加方法 createIteration 用以返回迭代器;

2020-2-23-21-10-38.png

  • 之后在 Menu 和 MenuItem 中分别实现这个方法:

2020-2-23-21-11-12.png

  • 因为菜单项 MenuItem 没什么可遍历的,所以返回一个空迭代器;
  • 空迭代器的 hasNext() 永远返回 false;

2020-2-23-21-12-54.png

  • 实现组合迭代器 CompositeIterator;

2020-2-23-21-15-3.png

# 实现素食菜单

  • 现在有了追踪遍历过程的能力后。我们就可以挑选出所有的素食了;

2020-2-23-21-17-30.png

上次更新: 8/22/2020, 8:14:09 AM