遍历“容器”的优雅方法——总结迭代器模式
前言
本文主要是读书笔记的整理,自己总结的倒不多,做个记录
聚集(集合)的概念
如果能把多个普通类的对象聚在一起形成一个总体,这个总体就被称之为聚集(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、需要能够更有弹性地在菜单项之间游走。比方说:可能只需要遍历甜点菜单,或者可以遍历餐厅的整个菜单。
此时,需要一种新的设计模式来解决这个案例的难题——组合模式,参看:优雅的处理树状结构——组合模式总结
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!