前言
本文主要是读书笔记的整理, 自己总结的倒不多, 做个记录
聚集 (集合) 的概念
如果能把多个普通类的对象聚在一起形成一个总体, 这个总体就被称之为聚集(Aggregate), 举例子:
1, 在任何编程语言中: 数组都是最基本的聚集, 在 Java 中, 数组也是其他的 JAVA 聚集对象的设计基础.
2, 在 Java 里, JAVA 聚集对象都是实现了 java.util.Collection 接口的对象, 是 JAVA 对聚集概念的直接支持. 从 JDK 1.2 开始, JAVA 提供了多种现成的聚集 API, 包括 Vector,ArrayList,HashSet,HashMap,Hashtable,ConcurrentHashMap 等.
自定义容器的封闭需求
假如因业务需要, RD 定义了专属的数据元素的聚集, 还要把它提供给客户端, 让其调用(不特别强调, 也包括其他依赖服务). 但是有时候为了安全, RD 不想让客户端看到聚集的内部实现, 只是能让她们访问就可以了, 比如遍历等操作. 还有的时候, 客户端不需要了解具体实现, 能否让客户端跳开复杂的数据结构? 因为调用者们不需要了解实现方式, 只要能开箱即用即可.
为了解决这个问题, 那么就需要有一种策略能让客户端遍历这个聚集体的时候, 无法窥破 RD 存储对象的方式, 无需了解内部的复杂数据结构.
迭代器的引出 -- 茶餐厅和煎饼铺子合并的经典案例
有两个遗留的点餐系统, 包括一套餐厅点餐系统 -- 专门提供正餐, 和一个煎饼铺子点餐系统(不要纠结为啥煎饼摊也有点餐系统...)-- 专门提供早餐(除了早餐, 其他时间不开放).
现状
餐厅里有很多卖饭的窗口, 它们的业务是一块单独的实现, 隔壁煎饼铺的业务, 也是一块单独的实现. 现在有个老板想把它们收购并合并, 让客户能在一个地方, 一个时间段内, 同时吃煎饼和餐厅的各种菜. 目前餐厅内有至少两家餐馆都统一实现了 MenuItem 类 -- 菜单子系统的菜单类.
问题
但是煎饼的菜单系统用的 ArrayList 记录菜单, 而餐厅的 RD 用的是数组实现了菜单系统, 双方的 RD, 都不愿意花费时间修改自己的实现. 毕竟有很多其他服务依赖了菜单子系统, 如下 MenuItem 代码:
- /**
- * 餐厅的菜单都是午餐项目, 煎饼的菜单, 都是早餐项目, 但是它们都属于菜单, 即:
- * 都有菜品名称, 描述, 是否是素的, 价格等
- * 故设计这样一个类作为菜单项目类
- */
- public class MenuItem {
- String name;
- String description;
- public MenuItem(String name,
- String description) {
- this.name = name;
- this.description = description;
- }
- public String getName() {
- return name;
- }
- public String getDescription() {
- return description;
- }
- }
不同的餐厅使用了这个 MenuItem 类
- /**
- * 煎饼窗口的菜单
- */
- public class PancakeHouseMenu {
- private List<MenuItem> menuItems; // menuItems 使用 ArrayList 存储菜单的项目, 动态数组, 使其很容易扩大菜单规模
- /**
- * 在构造菜单的时候, 把菜单加入到 ArrayList menuItems
- */
- public PancakeHouseMenu() {
- menuItems = new ArrayList<>();
- addItem("K&B's Pancake Breakfast",
- "Pancakes with scrambled eggs, and toast");
- addItem("Regular Pancake Breakfast",
- "Pancakes with fried eggs, sausage");
- }
- public void addItem(String name, String description) {
- MenuItem menuItem = new MenuItem(name, description);
- menuItems.add(menuItem);
- }
- public List<MenuItem> getMenuItems() {
- return menuItems;
- }
- }
- ///////////////////////////////////////////////////////////////
- /**
- * 餐厅的菜单
- */
- public class DinerMenu {
- private static final int MAX_ITEMS = 6;
- private int numberOfItems = 0;
- private MenuItem[] menuItems; // 使用了真正的数组实现菜单项的存储
- public DinerMenu() {
- menuItems = new MenuItem[MAX_ITEMS];
- addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat");
- addItem("BLT", "Bacon with lettuce & tomato on whole wheat");
- addItem("Soup of the day", "Soup of the day, with a side of potato salad");
- }
- public void addItem(String name, String description) {
- MenuItem menuItem = new MenuItem(name, description);
- if (numberOfItems>= MAX_ITEMS) {
- System.err.println("Sorry, menu is full! Can't add item to menu");
- } else {
- menuItems[numberOfItems] = menuItem;
- numberOfItems = numberOfItems + 1;
- }
- }
- public MenuItem[] getMenuItems() {
- return menuItems;
- }
- }
两种不同的菜单表现方式, 会给客户端调用带来很多问题, 假设客户端是服务员类 --Waitress, 下面是客户端的业务:
打印出菜单上的每一项: 打印每份菜单上的所有项, 必须调用 PancakeHouseMenu 和 DinerMenu 的 getMenuItem 方法, 来取得它们各自的菜单项, 但是两者返回类型是不一样的
只打印早餐项 (PancakeHouseMenu 的菜单) 或者只打印午餐项(DinerMenu 的菜单)
想要打印 PancakeHouseMenu 的项, 我们用循环将早餐 ArrayList 内的项列出来
想要打印 DinerMenu 的项目, 我们用循环将数组内的项一一列出来
打印所有的素食菜单项
指定项的名称, 如果该项是素食的话, 返回 true, 否则返回 false
实现 Waitress 的其他方法, 做法都和上面的方法类似, 发现 Waitress 处理两个菜单时, 总是需要写两个形式相似的循环, 去遍历这些菜单, 而且一旦外部菜单的数据结构变了, 客户端也得跟着修改.
再有, 如果还有第三家餐厅合并, 而且坑爹的是, 它以完全不同的实现方式实现了菜单...... 那怎么办? 此时难道还继续写第三个循环么......
以后, 这样甚至能发展到 N 个不同形式的循环......
这显然是非常不好的设计, 直接导致后期系统的大量垃圾代码和日益艰巨的维护任务.
为什么出现这种结局?
封装特性 | 面向接口编程 | 代码冗余 |
---|---|---|
Waitress (也就是客户端)竟然能非常清晰的, 而且是必须清晰的熟悉服务端的实现,这是很不科学的 | PancakeHouseMenu 和 DinerMenu 都没有面向接口编程, 而直接实现了具体业务,导致扩展困难 | DinerMenu 和 PancakeHouseMenu 都有很大重复代码, 没有抽象共享 |
那么可以解决么?
解决方法
1,Waitress 要遍历早餐项, 需要使用 ArrayList 的 size() 和 get() 方法
2,Waitress 遍历午餐项, 需要使用数组的 length 字段和中括号
现在创建一个新的对象, 将它称为迭代器(Iterator), 利用它来封装 "遍历集合内的每个对象的过程", 下面对其抽象, 封装.
原则: 只封装变化的部分
案例中变化的部分: 因为不同的集合实现, 导致的不同的遍历方式. 将其封装即可, 其实, 这正是迭代器模式的应用. 迭代器 Iterator, 是面向接口编程, 故它依赖于一个称为迭代器的接口:
- /**
- * 迭代器的接口, 一旦有了这个接口, 就可以为给种对象集合实现迭代器: 数组, 列表, 散列表等等
- */
- public interface Iterator {
- /**
- * 聚集中, 是否还有元素
- */
- boolean hasNext();
- /**
- * 返回聚集中的下一个元素
- */
- Object next();
- }
让餐厅实现迭代器接口 -- Iterator, 打造一个餐厅菜单迭代器 --DinerMenuIterator
- /**
- * 餐厅的迭代器
- */
- public class DinerMenuIterator implements Iterator {
- private MenuItem[] items;
- private int position = 0;
- public DinerMenuIterator(MenuItem[] items) {
- this.items = items;
- }
- public Object next() {
- MenuItem menuItem = items[position];
- position = position + 1;
- return menuItem;
- }
- public boolean hasNext() {
- return position <items.length && items[position] != null;
- }
- }
改造具体餐厅的菜单旧实现, 把之前的如下代码删掉, 因为它会暴露餐厅菜单的内部数据结构 menuItems
- public MenuItem[] getMenuItems() {
- return menuItems;
- }
下面是改造之后的餐厅菜单实现, PancakeHouseMenu 实现类似.
- public class DinerMenu {
- private static final int MAX_ITEMS = 6;
- private int numberOfItems = 0;
- private MenuItem[] menuItems;
- // 实现方式不变
- public DinerMenu() {
- menuItems = new MenuItem[MAX_ITEMS];
- addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat");
- addItem("BLT", "Bacon with lettuce & tomato on whole wheat");
- addItem("Soup of the day", "Soup of the day, with a side of potato salad");
- }
- // 实现方式不变
- public void addItem(String name, String description) {
- MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
- if (numberOfItems>= MAX_ITEMS) {
- System.err.println("Sorry, menu is full! Can't add item to menu");
- } else {
- menuItems[numberOfItems] = menuItem;
- numberOfItems = numberOfItems + 1;
- }
- }
- // 不需要 getMenuItems 方法, 因为它会暴露内部实现, 返回的直接是菜单的数据结构
- // 这个新方法代替 getMenuItems,createIterator 返回的是迭代器接口
- public Iterator createIterator() {
- return new DinerMenuIterator(menuItems);
- }
- }
这样写客户端的代码就不会重复两遍, 如下, 把迭代器的代码整合到 Waitress, 改掉之前冗余的循环遍历代码, 只需要传入一个迭代器作为遍历方法的参数, 把遍历聚集的工作, 委托给迭代器实现. 既能保护内部实现, 也能抽象遍历形式, 精简代码. 也符合了开闭原则 -- 以后菜单的实现逻辑修改了, 客户端也不用修改调用的代码.
- public class Waitress {
- // 服务员依赖的菜单系统
- private PancakeHouseMenu pancakeHouseMenu;
- private DinerMenu dinerMenu;
- public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
- this.pancakeHouseMenu = pancakeHouseMenu;
- this.dinerMenu = dinerMenu;
- }
- /**
- * 遍历全部菜单, 无需在客户端里积压多个重复的循环代码, 也符合了开闭原则 -- 以后修改遍历逻辑, 客户端不需要修改
- */
- public void printMenu() {
- // 为每个菜单系统, 创建一个迭代器 Iterator
- Iterator pancakeIterator = pancakeHouseMenu.createIterator();
- Iterator dinerIterator = dinerMenu.createIterator();// 把迭代器子类型, 传入
- printMenu(pancakeIterator);// 把迭代器子类型, 传入
- printMenu(dinerIterator);
- }
- /**
- * 接口的用法, 向上转型
- */
- private void printMenu(Iterator iterator) {
- // 先判断是否还能继续迭代
- while (iterator.hasNext()) {
- // Iterator 接口里 next 返回的是 Object 对象, 故需要强制转换
- MenuItem menuItem = (MenuItem) iterator.next();
- System.out.print(menuItem.getName() + ",");
- System.out.println(menuItem.getDescription());
- }
- }
- }
- //////
- public class MenuTestDrive {
- public static void main(String args[]) {
- PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
- DinerMenu dinerMenu = new DinerMenu();
- Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
- waitress.printMenu();
- }
- }
到底解决了什么问题
经迭代器模式对菜单系统进行封装, 使得各个餐厅的菜单系统能维持不变, 磨平了实现的差别, 减少了重写的工作量.
旧版代码的客户端 | 基于迭代器模式封装服务后,重写的客户端 |
---|---|
遍历:需要多个代码重复度较高的循环来实现,代码冗余度很高,加大无意义的工作量 | 只需要增加类,去实现各个菜单系统的迭代器,客户端只需要一个循环就能搞定所有的菜单服务调用 |
各个菜单系统的具体实现,封装的不行,对客户端暴露了数据结构,这是没有任何必要的 | 菜单的具体实现被封装,对外只公开迭代器,客户端不知道,也不需要知道具体菜单的实现 |
客户端被捆绑到了多个菜单实现类,牵一发动全身 | 客户端可以只用 iterator 接口做参数,通过向上转型,摆脱多个具体实现的捆绑,实现解耦 |
继续发现问题
客户端 Waitress 组合了多个具体实现类, 仍然会牵一发动全身, 比如修改了菜单的类名, 客户端就失效, 也需要修改, 仍然重度依赖
而且, 具体菜单的实现类又有共同的方法 createIterator , 完全可以进一步抽象.
改进上述设计 -- 充分利用 JDK 自带的迭代器
首先不再为 List 这样的数据结构重新实现迭代器, 因为 JDK 5 之后, Java 已经给我们实现好了, 对于 JDK 5 之后的所有集合容器, 都可以采用 JDK 自带的迭代器接口 --java.util,Itreator, 所以我们就不用自己写, 只需实现数组的迭代器即可.
1, 记住: JDK 不支持为数组生成迭代器
2,java.util 包中的 Collection 接口 --Java 所有的集合都实现了该接口, 该接口有迭代器方法.
继续改进 -- 抽象具体类的公共部分
可以为各个菜单实现类, 提供一个公共的接口 --Menu
原则: 面向接口编程
有多个具体实现类的时候, 要首先考虑不针对实现编程, 而是面向接口编程, 除非有共同的抽象方法 + 属性时, 可以考虑抽象父类. 本案例中, 只需使用接口, 就可以减少客户端 waittress 和具体菜单实现类之间的依赖.
- import java.util.Iterator;
- /**
- * 菜单系统要实现的方法, 抽象为接口
- */
- public interface Menu {
- Iterator createIterator();
- }
- /////////////////////////////
- /**
- * 菜单的每项, 抽象为类
- */
- public class MenuItem {
- private String name;
- private String description;public MenuItem(String name,
- String description) {
- this.name = name;
- this.description = description;
- }
- public String getName() {
- return name;
- }
- public String getDescription() {
- return description;
- }
- }
- /////////////////////////////////
- // 餐厅菜单系统的迭代器, 不需要实现额外声明的迭代器接口, 而是重写 JDK 的迭代器即可
- import java.util.Iterator;
- /**
- * 重写 JDK 的迭代器
- * implements java.util.Iterator;
- */
- public class DinerMenuIterator implements Iterator {
- private MenuItem[] items;
- private int position = 0;
- public DinerMenuIterator(MenuItem[] items) {
- this.items = items;
- }
- // 不需要变
- @Override
- public Object next() {
- MenuItem menuItem = items[position];
- position = position + 1;
- return menuItem;
- }
- // 不需要变
- @Override
- public boolean hasNext() {
- return position <items.length && items[position] != null;
- }
- // 重新实现, 最好是重写
- @Override
- public void remove() {
- if (position <= 0) {
- throw new IllegalStateException
- ("You can't remove an item until you've done at least one next()");
- }
- // 删除线性表的元素, 所有元素需要往前移动一个位置
- if (items[position - 1] != null) {
- for (int i = position - 1; i < (items.length - 1); i++) {
- items[i] = items[i + 1];
- }
- items[items.length - 1] = null;
- }
- }
- }
- ////////////////////
- // 餐厅菜单系统
- import java.util.Iterator;
- /**
- * Created by wangyishuai on 2018/1/27
- */
- public class DinerMenu implements Menu {
- private static final int MAX_ITEMS = 6;
- private int numberOfItems = 0;
- private MenuItem[] menuItems;
- // 实现方式不变
- public DinerMenu() {
- menuItems = new MenuItem[MAX_ITEMS];
- addItem("Soup of the day",
- "Soup of the day, with a side of potato salad");
- }
- // 实现方式不变
- public void addItem(String name, String description,
- boolean vegetarian, double price) {
- MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
- if (numberOfItems>= MAX_ITEMS) {
- System.err.println("Sorry, menu is full! Can't add item to menu");
- } else {
- menuItems[numberOfItems] = menuItem;
- numberOfItems = numberOfItems + 1;
- }
- }
- // 返回的是 java.util.Iterator;
- @Override
- public Iterator createIterator() {
- return new DinerMenuIterator(menuItems);
- }
- }
- /////////////////////////////////////////
- // 煎饼, 不需要再实现迭代器, 因为使用的数据结构是 JDK 的容器, 而对于 JDK 自带的集合容器, 不需要自己实现迭代器
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.List;
- /**
- * 对于 JDK 的集合容器 --List, 不需要 RD 实现迭代器
- */
- public class PancakeHouseMenu implements Menu {
- private List<MenuItem> menuItems;
- public PancakeHouseMenu() {
- menuItems = new ArrayList<>();
- addItem("K&B's Pancake Breakfast","Pancakes with scrambled eggs, and toast");
- }
- public void addItem(String name, String description) {
- MenuItem menuItem = new MenuItem(name, description);
- menuItems.add(menuItem);
- }
- @Override
- public Iterator createIterator() {
- // 返回 JDK ArrayList 自带的迭代器 iterator() 方法
- return menuItems.iterator();
- }
- }
- //////////////////////////////// 客户端
- import java.util.Iterator;
- public class Waitress {
- // 服务员依赖的菜单系统 -- 通过接口解耦合
- private Menu pancakeHouseMenu;
- private Menu dinerMenu;
- // 修改为 Menu 接口, 向上转型, 解耦合
- public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
- this.pancakeHouseMenu = pancakeHouseMenu;
- this.dinerMenu = dinerMenu;
- }
- /**
- * 以后修改遍历逻辑, 客户端不需要修改
- *// 不用修改
- */
- public void printMenu() {
- // 为每个菜单系统, 创建迭代器
- // java.util.Iterator;
- Iterator pancakeIterator = pancakeHouseMenu.createIterator();
- Iterator dinerIterator = dinerMenu.createIterator();
- printMenu(pancakeIterator);
- printMenu(dinerIterator);
- }
- /**
- * 接口的用法, 向上转型
- *// 不用修改
- * java.util.Iterator;
- */
- private void printMenu(Iterator iterator) {
- // 先判断是否还能继续迭代
- while (iterator.hasNext()) {
- // Iterator 接口里 next 返回的是 Object 对象, 故需要强制转换
- MenuItem menuItem = (MenuItem) iterator.next();
- System.out.print(menuItem.getName() + ",");
- System.out.println(menuItem.getDescription());
- }
- }
- }
- public class MenuTestDrive {
- public static void main(String args[]) {
- Menu pancakeHouseMenu = new PancakeHouseMenu();
- Menu dinerMenu = new DinerMenu();
- // 即使具体的菜单实现类修改了名字或者环了实现类, 客户端 --Waitress 也不需要修改代码, 解了耦合
- Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
- waitress.printMenu();
- }
- }
针对 JDK 的迭代器重写的原则
remove 方法应不应该重写
虽然对于客户端来说, remove 方法非必须(当然业务需要的话, 就必须自定义重写 remove), 但是最好还是提供该方法, 因为 JDK 的 Iterator 接口里包含了该方法, 如果不一起重写, 可能会出问题.
如果客户端真的不需要删除元素, 那么最好也重写该方法, 只需要在重写的时候抛出一个自定义的 (或者现成的) 异常 -- 如果有调用, 就提醒客户端不能删除元素. JDK 也是这样设计的, 默认抛出异常 UnsupportedOperationException
线程安全问题
默认的迭代器接口是线程不安全的, 如果有需要, 要额外的加强线程安全.
迭代器模式的标准概念
迭代器模式又叫游标 (Cursor) 模式, Iterator 模式, 迭代子模式...... 是对象的行为模式之一, 它把对容器中包含的内部对象的访问委托给外部的类, 让外部的类可以使用 Iterator 按顺序进行遍历访问, 而又不暴露其内部的数据结构.
Iterator Pattern (Another Name: Cursor)
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
脱离 Java 的领域, 那么可以认为: 迭代器模式可以顺序地访问聚集中的元素, 而不必暴露聚集的内部状态(internal representation). 它把遍历的责任转移到了迭代器, 而不是聚集本身, 简化了聚集的接口和实现代码, 也分割了责任.
迭代器模式的角色
Iterator(迭代器接口): 该接口必须定义实现迭代功能的最小定义方法集, 比如提供 hasNext()和 next()方法.
ConcreteIterator(具体的迭代器实现类): 迭代器接口 Iterator 的实现类. 可以根据具体情况加以实现.
Aggregate(聚集的接口): 定义基本功能以及提供类似 Iterator iterator()的方法.
concreteAggregate(聚集接口的实现类): 容器接口的实现类. 必须实现生成迭代器的方法.
聚集体如果不使用 Iterator 模式, 会存在什么问题
聚集类承担了太多功能
如果是自定义的聚集, 那么需要由聚集自己实现顺序遍历的方法 -- 直接在聚集的类里添加遍历方法. 这样, 容器类承担了太多功能:
一方面需要提供添加, 删除等本身应有的功能;
一方面还需要提供遍历访问功能.
不仅责任不分离, 还和客户端耦合太强
暴露聚集的太多内部实现细节
如果不使用迭代器模式, 那么需要客户端自己实现服务的遍历(联系餐厅和煎饼屋的合并案例), 会直接暴露聚集的数据结构, 往往这是不必要的, 客户端不需要了解服务的具体实现, 也是为了程序的安全 -- 不暴露太多的内部细节给客户端.
遍历聚集的时候修改聚集的元素, 引起聚集的状态混乱
如果使用的是 JDK 的集合类, 如果直接遍历, 且遍历的时候对集合修改, 会有异常抛出. 因为, 往往容器在实现遍历的过程中, 需要保存遍历状态, 当遍历操作和元素的添加, 删除等操作夹杂在一起, 这些更新功能在遍历的时候也被调用, 很容易引起集合的状态混乱和程序运行错误等. 此时应该为聚集使用迭代器模式, 如果是 JDK 的集合类, 就直接使用自带的迭代器进行迭代.
记住: Java 中的 foreach 循环看起来像一个迭代器, 但实际上并不是, 还是要使用迭代器模式
Iterator 支持从源集合中安全地删除对象, 只需在 Iterator 上调用 remove() 即可. 这样做的好处是可以避免 ConcurrentModifiedException , 这个异常顾名思意: 当打开 Iterator 迭代集合时, 同时又在对集合进行修改. 有些集合不允许在迭代时删除或添加元素, 但是调用 Iterator 的 remove() 方法是个安全的做法.
- List<String> list = new ArrayList<>(Arrays.asList("a","b","c","d"));
- for(String s : list){
- if(s.equals("a")){
- list.remove(s);
- }
- }
- // 会抛出一个 ConcurrentModificationException 异常, 相反下面的显示正常
- List<String> list = new ArrayList<>(Arrays.asList("a","b","c","d"));
- Iterator<String> iter = list.iterator();
- while(iter.hasNext()){
- String s = iter.next();
- if(s.equals("a")){
- iter.remove();
- }
- }
- // next() 必须在 remove() 之前调用.
- // 在 foreach 中, 编译器会使 next() 在删除元素之后被调用, 因此就会抛出 ConcurrentModificationException 异常
参考
1,Iterator 的 remove 方法可保证从源集合中安全地删除对象(转)
2, 正确遍历删除 List 中的元素方法 http://www.jb51.net/article/98763.htm
Fail Fast 问题
如果一个算法开始之后, 它的运算环境发生变化, 使得算法无法进行必需的调整时, 这个算法就应当立即发出故障信号. 这就是 Fail Fast 的含义. 同理, 如果聚集的元素在一个动态迭代子的迭代过程中发生变化, 迭代过程会受到影响而变得不能自恰. 这时候, 迭代子就应立即抛出一个异常. 这种迭代子就是实现了 Fail Fast 功能的迭代子.
使用迭代器模式的优点
Iterator 模式就是为了有效地处理按顺序进行遍历访问的一种设计模式, 简单地说, Iterator 模式提供一种有效的方法, 可以屏蔽聚集对象的容器类的实现细节, 而能对容器内包含的对象元素按顺序进行有效的遍历访问. 所以, Iterator 模式的应用场景可以归纳为以下几个:
访问容器中包含的内部对象
按顺序访问
优点总结:
1, 实现功能分离, 简化聚集的接口. 让聚集只实现本身的基本功能, 把迭代功能委托给外部类实现, 符合类的单一职责设计原则.
2, 隐藏聚集的实现细节, 符合最小知道原则. 为聚集或其子容器提供了一个统一接口, 一方面方便客户端调用; 另一方面使得客户端不必关注迭代器的实现细节.
3, 可以为聚集或其子容器实现不同的迭代器, 搭配其他设计模式, 比如策略模式等, 可以很容易的切换.
4, 客户端可以同时使用多个迭代器遍历一个聚集.
内部迭代器和外部迭代器
截止到此处, 都是分析的外部迭代器模式 -- 客户端来调用 next 方法, 去取得下一个元素.
相反, 内部迭代器是由迭代器自己控制游标, 在这种情况下, 必须告诉迭代器在游标移动的过程中, 要做什么事情 -- 必须将操作传给迭代器, 因为内部迭代器的客户端, 无法控制遍历过程, 所欲内部迭代器伸缩性不强, 一般不使用.
List 迭代的方向问题
都知道, next 方法是正向遍历, 那么自然可以实现反向遍历, 新加一个取得前一个元素的方法 + 一个判断游标是否已经走到了首节点的方法即可解决.
JDK 也为我们做了实现: ListIterator 接口, 提供了一个 previous 方法, JDK 中的任何实现了 List 接口的集合, 都可以实现反向迭代.
非线性数据结构的迭代问题
澄清一个问题 -- 迭代器模式是没有约束元素顺序的, 即 next (previous)只是取出元素, 并不是强制元素取出的先后顺序等价于元素的某种排序. 通俗的说, 不论是线性结构还是非线性的, 甚至是包含重复元素的结构, 除非有特殊业务需求, 都能对其实现迭代器模式.
不可幻想: 迭代的顺序就等价于集合中元素的某种有意义的排序, 两者没有必然关系, 谨记以避免做出错误判断, 除非有自定义的顺序约束.
单一职责设计原则和迭代器模式
设计原则: 一个类只有一个引起变化的原因. 如果有一个类具有两个改变的原因, 那么这会使得将来该类的变化机率上升, 而当它真的改变时, 你的设计中同时又有两个方面将会受到影响.
高内聚> 单一职责原则
内聚: 用来度量一个类或者模块紧密的达到了单一职责的目的(or 责任). 当一个类或者一个模块被设计为只支持一组相关的功能的时候, 就说它具有高内聚的特性, 反之就是低内聚的.
高内聚是一个比单一职责更普遍的概念, 即遵守了高内聚的类, 也同样具有单一职责.
迭代器模式就遵循了单一职责原则
其实前面的分析已经很全面, 迭代器模式, 分离了聚集的迭代的责任, 有效的契合了单一职责设计原则.
扩展案例: 合并咖啡厅的菜单系统
为其合并后的系统, 增加咖啡厅的菜单, 供应晚餐. 下面是咖啡厅的菜单系统实现:
- import java.util.HashMap;
- import java.util.Map;
- /**
- * 原始的咖啡厅菜单实现类
- */
- public class CafeMenu {
- /**
- * 菜单使用了 hash 表存储, 和现有的两个菜单系统实现不一样
- */
- private Map<String, MenuItem> menuItems = new HashMap<>();
- public CafeMenu() {
- addItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries");
- }
- public void addItem(String name, String description) {
- MenuItem menuItem = new MenuItem(name, description);
- menuItems.put(menuItem.getName(), menuItem);
- }
- public Map<String, MenuItem> getItems() {
- return menuItems;
- }
- }
- //////////////////////////////////////////////////
- public class MenuItem {
- private String name;
- private String description;public MenuItem(String name,
- String description) {
- this.name = name;
- this.description = description;
- }
- public String getName() {
- return name;
- }
- public String getDescription() {
- return description;
- }
- }
将咖啡厅菜单系统合并到现有的系统:
- public interface Menu {
- Iterator createIterator();
- }
- //////////////////////// 咖啡厅菜单系统
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.Map;
- /**
- * 合并之后的咖啡厅菜单实现类
- * hash 表也实现了 JDK 的迭代器, 不需要 RD 自己实现
- */
- public class CafeMenu implements Menu {
- /**
- * 菜单使用了 hash 表存储, 和现有的两个菜单系统实现不一样
- * 实现不变
- */
- private Map<String, MenuItem> menuItems = new HashMap<>();
- // 实现不变
- public CafeMenu() {
- addItem("Veggie Burger and Air Fries",
- "Veggie burger on a whole wheat bun, lettuce, tomato, and fries");
- }
- // 实现不变
- public void addItem(String name, String description) {
- MenuItem menuItem = new MenuItem(name, description);
- menuItems.put(menuItem.getName(), menuItem);
- }
- /**
- * hash 表支持 JDK 自带的迭代器 java.util.Iterator;
- */
- @Override
- public Iterator createIterator() {
- // 返回 java.util.Iterator; 只需要取得 hash 表的 value 集合即可
- return menuItems.values().iterator();
- }
- }
- //////////////////////// 客户端 Waitress
- import java.util.Iterator;
- public class Waitress {
- private Menu pancakeHouseMenu;
- private Menu dinerMenu;
- // 需要增加 cafeMenu
- private Menu cafeMenu;
- /**
- * 如果, 太多的参数, 可以使用建造者模式优化构造器
- * 需要增加 cafeMenu 参数
- */
- public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
- this.pancakeHouseMenu = pancakeHouseMenu;
- this.dinerMenu = dinerMenu;
- this.cafeMenu = cafeMenu;
- }
- /**
- * 需要增加 cafeMenu 的迭代器
- */
- public void printMenu() {
- Iterator pancakeIterator = pancakeHouseMenu.createIterator();
- Iterator dinerIterator = dinerMenu.createIterator();
- Iterator cafeIterator = cafeMenu.createIterator();
- printMenu(pancakeIterator);
- printMenu(dinerIterator);
- printMenu(cafeIterator);
- }
- /**
- * 无需修改
- */
- private void printMenu(Iterator iterator) {
- while (iterator.hasNext()) {
- MenuItem menuItem = (MenuItem) iterator.next();
- System.out.print(menuItem.getName() + ",");
- System.out.print(menuItem.getPrice() + "--");
- System.out.println(menuItem.getDescription());
- }
- }
- }
- //////////////////////// 测试
- public class MenuTestDrive {
- public static void main(String args[]) {
- Menu pancakeHouseMenu = new PancakeHouseMenu();
- Menu dinerMenu = new DinerMenu();
- Menu cafeMenu = new CafeMenu();
- Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu, cafeMenu);
- waitress.printMenu();
- }
- }
继续发现系统的问题 -- 客户端违反了开闭原则
合并咖啡厅的过程中, 发现每次合并新菜单, 都要打开客户端, 修改代码...... 客户端实现很丑陋, 违反了开闭原则.
虽然我们抽象了菜单, 让其在客户端解耦, 并且为菜单系统分别实现了迭代器, 让迭代责任分离, 对客户端隐藏了具体实现, 使用同一的迭代器接口, 解耦了迭代动作. 但是, 仍然将菜单处理分成独立的对象看待, 导致每次扩展, 都需要修改客户端 -- 客户端需要反复写: 调用 printMenue 的代码, 代码冗余严重, 而且每次都要给构造器增加新参数.
需要一种更好的办法 -- 集中管理菜单, 使其使用一个迭代器即可应付菜单的扩展
解决方案: 抽象客户端各个独立的菜单系统, 只需保留一个迭代器
使用现成的 ArrayList 类实现:
- import java.util.Iterator;
- import java.util.List;
- public class Waitress1 {
- /**
- * 把各个菜单系统集中到一个 list, 充分利用 list 的迭代器
- * 只需要一个类就搞定, 不再每次都 add 一个菜单类了
- */
- private List<Menu> menus;
- public Waitress1(List<Menu> menus) {
- this.menus = menus;
- }
- public void printMenu() {
- // 取得 list 的迭代器, 直接使用一个迭代器, 就能遍历所有菜单, 不需要在修改
- Iterator menuIterator = menus.iterator();
- while (menuIterator.hasNext()) {
- Menu menu = (Menu) menuIterator.next();
- printMenu(menu.createIterator());
- }
- }
- // 代码不需要变
- void printMenu(Iterator iterator) {
- while (iterator.hasNext()) {
- MenuItem menuItem = (MenuItem) iterator.next();
- System.out.print(menuItem.getName() + ",");
- System.out.print(menuItem.getPrice() + "--");
- System.out.println(menuItem.getDescription());
- }
- }
- }
基于迭代器模式实现的菜单系统无法实现树状菜单(无法扩展子菜单)
现在希望能够加上一份餐后甜点 "子菜单" 作为晚餐的饭后补充. 如果我们能让甜点菜单变成餐厅菜单集合的一个子元素, 就可以完美的解决. 但是根据现在的实现, 根本做不到. 因为饭后甜点子菜单的实现基于数组 -- 不变的, 类型不同, 无法扩展. 生产环境中, 这样的系统非常复杂, 更加困难.
解决方案 -- 树
1, 需要某种树形结构, 可以容纳菜单, 子菜单和菜单项.
2, 需要确定能够在每个菜单的各个项目之间游走, 而且至少要像现在用迭代器一样方便.
3, 需要能够更有弹性地在菜单项之间游走. 比方说: 可能只需要遍历甜点菜单, 或者可以遍历餐厅的整个菜单.
此时, 需要一种新的设计模式来解决这个案例的难题 -- 组合模式, 参看: 优雅的处理树状结构 -- 组合模式总结
来源: https://www.cnblogs.com/kubixuesheng/p/5183739.html