9.迭代器与组合模式
该模式用于管理良好的集合
有许多种方法可以把对象堆起来成为一个集合(collec-tion)。我们可以把它们放进数组、堆栈、列表或者是散列表(Hashtable)中。
每一种都有它自己的优点和适合的使用时机,但总有一个时候,你的客户想要遍历这些对象,而当他这么做时,你打算让客户看到我们的实现吗?
我们当然希望最好不要!这太不专业了。没关系,不要为你的工作担心,你将在本章中学习如何能让客户遍历你的对象而又无法窥视你存储对象的方式;也将学习如何创建一些对象超集合(super collection),能够一口气就跳过某些让人望而生畏的数据结构。你还将学到一些关于对象职责的知识。
9.1 餐厅 煎饼屋场景
煎饼屋和餐厅合并了,领导决定用煎饼屋的菜单当做早餐,用餐厅的菜单当做午餐,但是目前有个问题是煎饼屋的菜单是使用ArrayList实现的,而餐厅的菜单是用的数组。而且两个餐厅虽然合并在一起,但是没有人愿意做出改变。
我们先看看这两个餐厅的菜单有什么不同
每个菜单项上都有相应的名称,叙述和价格
于是我们先新建一个菜单类
package com.zhiyi.design.迭代器与组合模式;
public class MenuItem {
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;
}
}
为了节省篇幅,上面的代码节省了get和set方法
接下类我们看看煎饼屋的菜单实现
public class PancakeHouseMenu {
ArrayList menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("K&B煎饼早餐", "煎饼、炒蛋和吐司", true, 2.99);
addItem("普通煎饼早餐", "煎饼、煎蛋、香肠", false, 2.99);
addItem("蓝莓煎饼", "用新鲜蓝莓做成的煎饼", true, 3.59);
addItem("华夫饼干", "蓝莓或草莓冰", true, 3.59);
}
/**
* 要加入一个菜单项,煎饼屋的做法是创建一个新的菜单对象,传入每一个变量,然后将它加入ArrayList中
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
/**
* 返回菜单项列表
*
* @return
*/
public ArrayList getMenuItems() {
return menuItems;
}
}
餐厅的菜单
public class DinerMenu {
/**
* 餐厅的菜单使用的是一个数组,所以可以控制菜单的长度,并且在取出的菜单项的时候不需要转型
*/
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("素食BLT", "培根配生菜和西红柿小麦", true, 2.99);
addItem("BLT", "全麦培根配生菜和番茄", false, 2.99);
addItem("汤", "今天的汤,配一份土豆沙拉", false, 2.99);
addItem("热狗", "一个热狗,配沙罗,调味品,洋葱,用奶酪调味", false, 3.05);
}
/**
* aditem方法需要拿所有必要的参数来创建一个菜单项,并实例化它。这个方法也会检查数是否已经超出了它的长度限制。
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
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("抱歉,菜单已经满了,您无法继续添加");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
/**
* 返回一个菜单项的数组
*
* @return
*/
public MenuItem[] getMenuItems() {
return menuItems;
}
}
9.2 那么这两种不同的菜单表现方式,这会出现什么问题?
想了解为什么有两种不同的菜单表现方式会让事情变得复杂化,让我们试着实现一个同时使用这两个菜单的客户代码。
假设你已经被他们两个人合组的新公司雇用,你的工作是要创建一个Java版本的女招待(毕竟,这是对象村)。这个Java版本的女招待规格是:能应对顾客的需要打印定制的菜单,甚至告诉你是否某个菜单项是素食的,而无需询问厨师。这可是一大创新!
我们先大概看一下女招待员的规格
我们先从实现printMenu()方法开始
- 1.打印每份菜单上的所有项,必须调用PancakeHouseMenu和DinerMenu的getMenultem()方法,来取得它们各自的菜单项。请注意,两者的返回类型是不一样的。
/**
* 早餐项是在一个ArrayList中
*/
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList breakfastakeHouseMenu = pancakeHouseMenu.getMenuItems();
/**
* 午餐项是在一个数组中
*/
DinerMenu dinerMenu = new DinerMenu();
MenuItem[] lunchItems = dinerMenu.getMenuItems();
- 2.现在,想要打印PancakeHouseMenu的项,我们用循环将早餐ArrayList内的项一一列出来。想要打印DinerMenu的项目,我们用循环将数组内的项一一列出来。
/**
* 我们必须实现两个不同的for循环来处理这两个不同的菜单
*/
for (int i = 0; i < breakfastakeHouseMenu.size(); i++) {
MenuItem menuItem = (MenuItem) breakfastakeHouseMenu.get(i);
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
假如我们还有第三个餐厅合并进来,那么我们则还需要第三个for循环,所以针对以上的操作,我们违反了哪些设计的原则呢?
- 1.我们是针对PancakeHouseMenu和DinerMenu的具体实现编码,而不是针对接口。
- 2.女招待需要知道每个菜单如何表达内部的菜单项集合,这违反了封装。
- 3.我们有重复的代码:printMenu()方法需要两个循环,来遍历两种不同的菜单。如果我们加上第三种菜单,我们就需要第三个循环。
这是我们目前最能容易知道的违反的注意事项
这两个餐厅让我们很为难。他们都不想改变自身的实现,因为意味着要重写许多代码。但是如果他们其中一人不肯退让,我们就很难办了,我们所写出来的女招待程序将难以维护、难以扩展。
所以我们如果能够让他们的菜单实现一个相同的接口,这样一来,我们就可以最小化女招待代码中的具体引用,同时还有希望摆脱遍历这两个菜单所需的多个循环。
9.3 封装遍历
在对以往的设计原则中,我们务必牢记一个最基本也是最简单的设计原则那就是封装变化的部分,很明显,这里变化的部分是:由不同的集合类型所造成的遍历
-
1.要遍历早餐项,我们需要使用ArrayList的size()和get()方法
-
2.要遍历午餐项,我们需要使用数组的length字段和中括号
-
3.现在我们创建一个对象,将它称为迭代器(lterator),利用它来封装“遍历集合内的每个对象的过程”。先让我们在ArrayList上试试:
-
4.将我们的迭代器在数组上也试试
9.4 迭代器模式
我们将数组或者ArrayList转化为迭代器,这就是一个是设计模式叫做迭代器模式
关于迭代器模式,首先他依赖于一个迭代器接口
现在,一旦我们有了这个接口,就可以为各种对象集合实现迭代器:数组、列表、散列表……如果我们想要为数组实现迭代器,以便使用在DinerMenu中,看起来就像这样:
我们接下开始尝试如何将这个迭代器挂钩到DinerMenu中
- 1.创建一个迭代器接口
public interface Iterator {
/**
* hasNext()方法会返回一个布尔值,让我们知道是否还有更多的元素
*
* @return
*/
boolean hasNext();
/**
* 返回下一个元素
*
* @return
*/
Object next();
}
- 2.实现一个具体的迭代器为餐厅菜单服务
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;//position记录当前数组遍历的位置
/**
* 构造器需要传入一个菜单项的数组当做参数
*
* @param items
*/
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
@Override
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else return true;
}
@Override
public Object next() {
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
}
现在我们有了迭代器,我们只需要加入一个方法使其能够创建一个DinerMenuIterator,并将它返回给客户,所以我们将DinnerMenu修改成以下内容:
public class DinerMenu {
/**
* 餐厅的菜单使用的是一个数组,所以可以控制菜单的长度,并且在取出的菜单项的时候不需要转型
*/
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("素食BLT", "培根配生菜和西红柿小麦", true, 2.99);
addItem("BLT", "全麦培根配生菜和番茄", false, 2.99);
addItem("汤", "今天的汤,配一份土豆沙拉", false, 2.99);
addItem("热狗", "一个热狗,配沙罗,调味品,洋葱,用奶酪调味", false, 3.05);
}
/**
* aditem方法需要拿所有必要的参数来创建一个菜单项,并实例化它。这个方法也会检查数是否已经超出了它的长度限制。
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
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("抱歉,菜单已经满了,您无法继续添加");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
// /**
// * 返回一个菜单项的数组
// *
// * @return
// */
// public MenuItem[] getMenuItems() {
// return menuItems;
// }
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
}
此时我们已经无需在通过getMenuItems()方法来获取内容了
接下来我们继续完成PancakeHouseMenu的改造,参考餐厅的改造方式:
- 1.首先我们创建一个迭代器PancakeHouseIterator
public class PancakeHouseIterator implements Iterator {
ArrayList<MenuItem> items;
int position = 0;
public PancakeHouseIterator(ArrayList<MenuItem> menuItems) {
this.items = menuItems;
}
@Override
public boolean hasNext() {
if (position >= items.size() || items.get(position) == null) {
return false;
} else return true;
}
@Override
public Object next() {
MenuItem menuItem = items.get(position);
position = position + 1;
return menuItem;
}
}
- 2.在PancakeHouseMenu中加入一个方法使其能够创建迭代器
public class PancakeHouseMenu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("K&B煎饼早餐", "煎饼、炒蛋和吐司", true, 2.99);
addItem("普通煎饼早餐", "煎饼、煎蛋、香肠", false, 2.99);
addItem("蓝莓煎饼", "用新鲜蓝莓做成的煎饼", true, 3.59);
addItem("华夫饼干", "蓝莓或草莓冰", true, 3.59);
}
/**
* 要加入一个菜单项,煎饼屋的做法是创建一个新的菜单对象,传入每一个变量,然后将它加入ArrayList中
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
// /**
// * 返回菜单项列表
// *
// * @return
// */
// public ArrayList getMenuItems() {
// return menuItems;
// }
public Iterator createIterator() {
return new PancakeHouseIterator(menuItems);
}
}
接下来我们开始尝试将迭代器代码整合到女招待员中。我们应该摆脱原本冗余的部分。整合的做法相当直接:首先创建一个printMenu()方法
public class Waitress {
PancakeHouseMenu pancakeHouseMenu;
DinerMenu dinerMenu;
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
/**
* 为每个菜单创建一个各自的构造器
*/
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("早餐");
printMenu(pancakeIterator);
System.out.println("午餐");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.println(menuItem.getName() + ", ");
System.out.println(menuItem.getPrice() + ", ");
System.out.println(menuItem.getDescription() + ", ");
}
}
}
接下来我们进行测试
public class MenuTestDriver {
public static void main(String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
waitress.printMenu();
}
}
到目前为止我们做了什么
首先,我们让对象村的厨师们非常快乐。他们可以保持他们自己的实现又可以摆平差别。只要我们给他们这两个迭代器(PancakeHouseMenulterator和DinerMenulterator),他们只需要加入一个createIterator()方法,一切就大功告成了。
这个过程中,我们也帮了我们自己。女招待将会更容易维护和扩展。
其实读到此处你们可能会有些许的疑问,为什么不适用java自带的Iterator接口呢,其实主要目的就是让你能够了解如何从头创建一个迭代器,接下来我们回归到Java的Iterator接口
这一切都太简单了:我们只需将煎饼屋菜单迭代器和餐厅菜单迭代器所扩展的接口,由我们自己的迭代器接口,改成java.util的迭代器接口即可,对吧?差不多就这样……实际上,甚至更简单。其实不只java.util有迭代器接口连ArrayList也有一个返回一个迭代器的iterator()方法。换句话说,我们并不需要为ArrayList实现自己的迭代器。然而,我们仍然需要为餐厅菜单实现一个迭代器,因为餐厅菜单使用的是数组,而数组不支持iteratorQ方法(或其他创建数组迭代器的方法)。
在多线程的情况下,可能会有多个迭代器引用同一个对象集合。remove()会造成怎样的影响?
答:后果并没有指明,所以很难预料。当你的程序在多线程的代码中使用到迭代器时,必须特别小心,这看起来就和我们之前的定义一样。
9.5 利用java.util.Iterator
我们先从煎饼屋开始改造,修改PancakeHouseMenu如下
import java.util.ArrayList;
import java.util.Iterator;
public class PancakeHouseMenu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("K&B煎饼早餐", "煎饼、炒蛋和吐司", true, 2.99);
addItem("普通煎饼早餐", "煎饼、煎蛋、香肠", false, 2.99);
addItem("蓝莓煎饼", "用新鲜蓝莓做成的煎饼", true, 3.59);
addItem("华夫饼干", "蓝莓或草莓冰", true, 3.59);
}
/**
* 要加入一个菜单项,煎饼屋的做法是创建一个新的菜单对象,传入每一个变量,然后将它加入ArrayList中
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
public Iterator createIterator() {
return menuItems.iterator();
}
}
接着处理DinnerMenu
首先改造DinnerMenuIterator
import java.util.Iterator;
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;//position记录当前数组遍历的位置
/**
* 构造器需要传入一个菜单项的数组当做参数
*
* @param items
*/
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
@Override
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else return true;
}
@Override
public Object next() {
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
@Override
public void remove() {
if (position <= 0) {
try {
throw new IllegalAccessException("数组中没有任何数据,禁止移除");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
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;
}
}
}
接下来我们需要将女接待员从菜单中解耦
这个Menu接口我们只需要给菜单一个共同的接口,这个Menu接口相当简单,可能迟早需要在里面多加入一些方法,例如addltem(),但是目前,我们还是让厨师控制他们的菜单,不要把那些方法放在公开接口中:
import java.util.Iterator;
public interface Menu {
/**
* 让客户能够取得一个菜单项的迭代器
*
* @return
*/
public Iterator createIterator();
}
我们稍微改造一下PancakeHouseMenu和DinerMenu
- PancakeHouseMenu
import java.util.ArrayList;
import java.util.Iterator;
public class PancakeHouseMenu implements Menu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("K&B煎饼早餐", "煎饼、炒蛋和吐司", true, 2.99);
addItem("普通煎饼早餐", "煎饼、煎蛋、香肠", false, 2.99);
addItem("蓝莓煎饼", "用新鲜蓝莓做成的煎饼", true, 3.59);
addItem("华夫饼干", "蓝莓或草莓冰", true, 3.59);
}
/**
* 要加入一个菜单项,煎饼屋的做法是创建一个新的菜单对象,传入每一个变量,然后将它加入ArrayList中
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
@Override
public Iterator createIterator() {
return menuItems.iterator();
}
}
- DinerMenu
package com.zhiyi.design.迭代器与组合模式.interal;
import com.zhiyi.design.迭代器与组合模式.MenuItem;
import java.util.Iterator;
public class DinerMenu implements Menu {
/**
* 餐厅的菜单使用的是一个数组,所以可以控制菜单的长度,并且在取出的菜单项的时候不需要转型
*/
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("素食BLT", "培根配生菜和西红柿小麦", true, 2.99);
addItem("BLT", "全麦培根配生菜和番茄", false, 2.99);
addItem("汤", "今天的汤,配一份土豆沙拉", false, 2.99);
addItem("热狗", "一个热狗,配沙罗,调味品,洋葱,用奶酪调味", false, 3.05);
}
/**
* aditem方法需要拿所有必要的参数来创建一个菜单项,并实例化它。这个方法也会检查数是否已经超出了它的长度限制。
*
* @param name
* @param description
* @param vegetarian
* @param price
*/
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("抱歉,菜单已经满了,您无法继续添加");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
@Override
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
}
我们看看女仆的实现
import java.util.Iterator;
public class Waitress {
Menu pancakeHouseMenu;
Menu dinerMenu;
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("早餐");
this.printMenu(pancakeIterator);
System.out.println("午餐");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.println(menuItem.getName() + ", ");
System.out.println(menuItem.getPrice() + ", ");
System.out.println(menuItem.getDescription() + ", ");
}
}
}
这为我们带来了什么好处呢?
煎饼屋菜单和餐厅菜单的类,都实现了Menu接口,女招待可以利用接口 (而不是具体类)引用每一个菜单对象。这样,通过“针对接口编程,而不
针对实现编程”,我们就可以减少女招待和具体类之间的依赖。
9.6 定义迭代器模式
迭代器模式让我们能游走于聚合内的每一个元素,而又不暴露其内部的表示。把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。
9.7 单一责任
想知道为什么,首先你需要认清楚,当我们允许一个类不但要完成自己的事情(管理某种聚合),还同时要担负更多的责任(例如遍历)时,我们就给了这个类两个变化的原因。两个?没错,就是两个:如果这个集合改变的话,这个类也必须改变;如果我们遍历的方式改变的话,这个类也必须跟着改变。所以,再一次地,我们的老朋友“改变”又成了我们设计原则的中心。
下面在引入一个新的概念:内聚
内聚(cohesion)这个术语你应该听过,它用来度量一个类或模块紧密地达到单一目的或责任。
当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。
内聚是一个比单一责任原则更普遍的概念,但两者其实关系是很密切的。遵守这个原则的类容易具有很高的凝聚力,而且比背负许多责任的低内聚类更容易维护
我们下面通过一个例子判断这个类是高内聚还是低内聚:
9.8 整合晚餐(咖啡厅)
为了给员工提供晚餐,现在又决定将咖啡厅组合进来,供应晚餐菜单
下面我们先看一下咖啡厅的菜单
import java.util.Hashtable;
public class CafeMenu {
Hashtable menuItems = new Hashtable();
public CafeMenu() {
addItem("Vegggie Burger and Air Fries",
"tomato fries", true, 3.99);
addItem("Soup the day", "with side salad",
false, 3.69);
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.put(menuItem.getName(), menuItem);
}
public Hashtable getItems() {
return menuItems;
}
}
接下来我们要做的就是重构咖啡厅的代码,让它能够很好的和我们的迭代器融合进去
9.9 重做咖啡厅代码
由于HashTable本身就支持Java内置的迭代器,所以我们改造起来也是极其的容易
public class CafeMenu implements Menu {
Hashtable menuItems = new Hashtable();
public CafeMenu() {
addItem("Vegggie Burger and Air Fries", "tomato fries", true, 3.99);
addItem("Soup the day", "with side salad", false, 3.69);
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.put(menuItem.getName(), menuItem);
}
@Override
public Iterator createIterator() {
/**
* 虽然HashTable和ArrayList相比复杂很多,
* 因为它的每一个数据都是由一个key和一个value组成,尽管如此我们仍然可以获取值
*/
return menuItems.values().iterator();
}
}
在女侍中加入我们咖啡餐厅的菜单
import java.util.Iterator;
public class Waitress {
Menu pancakeHouseMenu;
Menu dinerMenu;
Menu cafeMenu;
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu,
CafeMenu cafeMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
this.cafeMenu = cafeMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
Iterator cafeIterator = cafeMenu.createIterator();
System.out.println("早餐");
this.printMenu(pancakeIterator);
System.out.println("午餐");
printMenu(dinerIterator);
System.out.println("晚餐");
printMenu(cafeIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.println(menuItem.getName() + ", ");
System.out.println(menuItem.getPrice() + ", ");
System.out.println(menuItem.getDescription() + ", ");
}
}
}
测试
public class MenuTestDriver {
public static void main(String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
CafeMenu cafeMenu = new CafeMenu();
Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu, cafeMenu);
waitress.printMenu();
}
}
9.10 我们做了哪些事情
9.11 迭代器与集合
我们所使用的这些类都属于Java Collection Framework的一部分。这里所谓的“framework”(框架)指的是一群类和接口,其中包括了ArrayList、Vector、LinkedList、Stack和PriorityQueue这些类都实现了java.util.Collection接口。这个接口包含了许多有用的方法,可以操纵一群对象。
我们看一下这个接口里面都有些什么
接下来我们继续回到Waitress
我们可以看出我们调用了三次printMenu(),不仅如此,每次我们一旦有新菜单的加入,就必须修改Waitress,加入更多的代码。
这不是女招待的错。对于将她从菜单的实现上解耦和提取遍历动作到迭代器,我们都做得很好。但我们仍然将菜单处理成分离而独立的对象——我们需要一种一起管理它们的方法。
9.12 改善Waitress
我们所要做的事就是将这些菜单全都打包进一个ArrayList,然后取得它的送代器,遍历每个菜单。这么一来。女招待的代码就变得很简单。而旦菜单再多也不怕了。
public class Waitress {
ArrayList menus;
public Waitress(ArrayList menus) {
this.menus = menus;
}
public void printMenu() {
Iterator menuIterator = menus.iterator();
while (menuIterator.hasNext()) {
Menu menu = (Menu) menuIterator.next();
printMenu(menu.createIterator());
}
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.println(menuItem.getName() + ", ");
System.out.println(menuItem.getPrice() + ", ");
System.out.println(menuItem.getDescription() + ", ");
}
}
}
虽然我们失去了菜单的名字,但是可以把名字加进每个菜单中。
我下面修改一个菜单来供大家参考
public class CafeMenu implements Menu {
Hashtable menuItems = new Hashtable();
String menuName;
public CafeMenu() {
addItem("Vegggie Burger and Air Fries", "tomato fries", true, 3.99);
addItem("Soup the day", "with side salad", false, 3.69);
menuName = "咖啡餐厅(晚餐)";
}
public void addItem(String name, String description, boolean vegetarian, double price) {
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.put(menuItem.getName(), menuItem);
}
@Override
public Iterator createIterator() {
/**
* 虽然HashTable和ArrayList相比复杂很多,
* 因为它的每一个数据都是由一个key和一个value组成,尽管如此我们仍然可以获取值
*/
return menuItems.values().iterator();
}
}
9.13 饭后甜点
现在怎么办?我们不仅仅要支持多个菜单,甚至还要支持菜单中的菜单。
如果我们能让甜点菜单变成餐厅菜单集合的一个元素,那该有多好。但是根据现在的实现,根本做不到。
如果想要实现上图所示的结构,我们需要怎么做呢
- 1.我们需要某种树形结构,可以容纳菜单、子菜单和菜单项。
- 2.我们需要确定能够在每个菜单的各个项之间游走,而且至少要像现在用迭代器一样方便。
- 3.我们也需要能够更有弹性地在菜单项之间游走。比方说,可能只需要遍历甜点菜单,或者可以遍历餐厅的整个菜单(包括甜点菜单在内)。
9.14 组合模式
这个模式能够创建一个树形结构,在同一个结构中处理嵌套菜单和菜单项组。
通过将菜单和项放在相同的结构中,我们创建了一个“整体/部分”层次结构,即由菜单和菜单项组成的对象树。但是可以将它视为一个整体,像是一个丰富的大菜单。
一旦有了丰富的大菜单,我们就可以使用这个模式来“统一处理个别对象和组合对象”。这意味着什么?
它意味着,如果我们有了一个树形结构的菜单、子菜单和可能还带有菜单项的子菜单,那么任何一个菜单都是一种“组合”。因为它既可以包含其他菜单,也可以包含菜单项。个别对象只是菜单项——并未持有其他对象。就像你将看到的,使用一个遵照组合模式的设计,让我们能够写出简单的代码,就能够对整个菜单结构应用相同的操作(例如打印!)。
接下来我们可以通过类图可以很直观的看出组合模式之间的联系
读到此处我们可能被组件、组合、树之间的区别和联系搞混了,下面我说一下他们之间的关联
组合包含组件。组件有两种:组合与叶节点元素。听起来象递归是不是?组合持有一群孩子,这些孩子可以是别的组合或者叶节点元素。当你用这种方式组织数据的时候,最终会得到树形结构(正确的说法是由上而下的树形结构)、根部是一个组合、而组合的分支逐渐往下延伸,直到叶节点为止。
9.15 利用组合设计菜单
我们要如何在菜单上应用组合模式呢?
一开始,我们需要创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。换句话说,我们可以针对菜单或菜单项调用相同的方法。
现在,对于菜单或菜单项来说,有些方法可能不太恰当。但我们可以处理这个问题,等一下就会这么做。至于现在,让我们从头来看看如何让菜单能够符合组合模式的结构;
9.15.1 实现菜单组件
所有的组件都必须实现MenuComponent接口,然而,叶结点和组合节点的角色不同.所以有些方法可能并不适合某种节
面对这种情况.有时候你最好是抛出运行时异常
- MenuComponent接口
/**
* MenuComponent对每个方法都提供默认的实现
* 因为有些方法对菜单有意义,而有些只对菜单有意义,默认实现是抛出UnsupportedOperationException
* 这样的话如果菜单项或者菜单不支持某个操作,他们就不需要做任何事情,直接继承默认实现就好
*/
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 boolean isVegetarian() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
9.15.2 实现菜单项
这是组合类图里的叶类,它实现组合内元素的行为。
public class MenuItem extends MenuComponent {
String name;
String description;
boolean isVegetarian;
double price;
public MenuItem(String name, String description, boolean isVegetarian, double price) {
this.name = name;
this.description = description;
this.isVegetarian = isVegetarian;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean isVegetarian() {
return isVegetarian;
}
public double getPrice() {
return price;
}
public void print() {
System.out.print(" " + getName());
if (isVegetarian) {
System.out.print("(v)");
}
System.out.print("," + getPrice());
System.out.println(" ---" + getDescription());
}
}
9.15.3 实现组合菜单
我们已经有了菜单项,还需要组合类,就是我们叫做菜单的。别忘了,此组合类可以持有菜单项或其他菜单。有一些方法并未在MenuComopnent类中实现,比如getPrice()和isVegertarian(),因为这些方法对菜单而言并没多大意义。
/**
* 菜单和菜单项一样,都是MenuComponent
*/
public class Menu extends MenuComponent {
/**
* 菜单可以有任意数目的孩子,这些孩子都必须属于MenuComponent类型
* 我们使用内部的ArrayList记录它们
*/
ArrayList menuComponents = new ArrayList();
String name;
String description;
/**
* 这个和我们之前的实现不一样,我们将给每个菜单一个名字和一个描述
* 每个菜单的类名称就是此菜单的名字
*
* @param name
* @param 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 (MenuComponent) menuComponents.get(i);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
public void print() {
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("----------------------");
}
}
但是如果我们通过上面的print()只能够得到一个简单的菜单名字和描述,而不是完整的打印出组合内的每一项。
因为菜单是一个组合,包含了菜单项和其他的菜单,所以它的print()应该打印出它所包含的一切。如果它不这么做,我们就必须遍历整个组合的每个节点,然后将每一项打印出来。这么一来,也就失去了使用组合结构的意义。
所以我们对print()方法作如下修改
public void print() {
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("----------------------");
/**
* 我们用了迭代器,用它来遍历所有菜单组件。。。
* 在遍历过程中,可能会遇到其他菜单或者某个菜单项。
* 由于菜单和菜单项都实现print(),那我们只要调用print()即可
*/
Iterator iterator = menuComponents.iterator();
while (iterator.hasNext()) {
MenuComponent menuComponent = (MenuComponent) iterator.next();
menuComponent.print();
}
}
}
请注意:在遍历期间,如果遇到另一个菜单对象.它的pint()方法会开始另一个遍历,依次类推。
接下来我们继续回到女招待身上,当我们使用了组合模式之后,我们就会发现她的代码会变得十分简洁
public class Waitress {
/**
* 现在女招待的代码变得已经十分简洁
* 现在我们只需要将最顶层的菜单组件交给他就可以了
* 最顶层的菜单包含其他所有菜单,所以我们称之为allMenus
*/
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
/**
* 她只需霎调用最顶层菜单的print()
* 就可以打印整个菜单层次,包括所有菜单及所有菜单项。
*/
public void printMenu() {
allMenus.print();
}
}
接下来开始进行测试
在测试之前我们看一下运行菜单组合是什么样的
接下来开始编写测试代码
public class MenuTestDriver {
public static void main(String[] args) {
/**
* 先创建所有的菜单对象
*/
MenuComponent pancakeHouse = new Menu("煎饼屋", "早餐");
MenuComponent dinerMenu = new Menu("午餐厅", "午餐");
MenuComponent cafeMenu = new Menu("咖啡厅", "晚餐");
MenuComponent dessertMenu = new Menu("甜品屋", "饭后甜点");
/**
* 创建一个顶层菜单
*/
MenuComponent allMenus = new Menu("All MENUS", "All menus combined");
/**我们使用组合的add()方法,将每个菜单都加入到顶层菜单的allMenus中**/
allMenus.add(pancakeHouse);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
allMenus.add(dessertMenu);
/**
* 加入菜单项
*/
/*pancakeHouse菜单项*/
pancakeHouse.add(new MenuItem("K&B煎饼早餐", "煎饼、炒蛋和吐司", true, 2.99));
pancakeHouse.add(new MenuItem("普通煎饼早餐", "煎饼、煎蛋、香肠", false, 2.99));
pancakeHouse.add(new MenuItem("蓝莓煎饼", "用新鲜蓝莓做成的煎饼", true, 3.59));
pancakeHouse.add(new MenuItem("华夫饼干", "蓝莓或草莓冰", true, 3.59));
/*dinerMenu菜单项*/
dinerMenu.add(new MenuItem("素食BLT", "培根配生菜和西红柿小麦", true, 2.99));
dinerMenu.add(new MenuItem("BLT", "全麦培根配生菜和番茄", false, 2.99));
dinerMenu.add(new MenuItem("汤", "今天的汤,配一份土豆沙拉", false, 2.99));
dinerMenu.add(new MenuItem("热狗", "一个热狗,配沙罗,调味品,洋葱,用奶酪调味", false, 3.05));
/*cafeMenu菜单项*/
cafeMenu.add(new MenuItem("Vegggie Burger and Air Fries", "tomato fries", true, 3.99));
cafeMenu.add(new MenuItem("Soup the day", "with side salad", false, 3.69));
/*dessertMenu菜单项*/
dessertMenu.add(new MenuItem("苹果派", "苹果派,有片状外壳,上面有香草冰淇淋", true, 1.69));
/**
* 一旦我们将整个莱单层次构造完毕把它整个交给女招待
* 你会发现,女招待要将整份菜单打印出来,简直就是易如反掌
*/
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
运行结果:
9.16 回到迭代器
我们其实已经在print()方法内部的实现中使用了迭代器,除此之外,如果女招待需要,我们也能让她使用迭代器遍历整个组合。比方说,女招待可能想要游走整个菜单,挑出素食项。想要实现一个组合迭代器,让我们为每个组件都加上createIterator()方法。从抽象的MenuComponent类开始下手:
现在我们需要在MenuComponent,菜单和菜单项类中实现这个方法
public Iterator createIterator() {
throw new UnsupportedOperationException();
}
package com.zhiyi.design.迭代器与组合模式.迭代器And组合模式;
import com.zhiyi.design.迭代器与组合模式.组合.MenuComponent;
import java.util.ArrayList;
import java.util.Iterator;
/**
* 菜单和菜单项一样,都是MenuComponent
*/
public class Menu extends MenuComponent {
public Iterator createIterator() {
return new CompositeIterator(menuComponents.iterator());
}
}
注意:和原来的代码唯一的不同是仅仅加入了createIterator()方法
MenuItem和Menu一样,和原来不同的也是只加入了createIterator()方法
public Iterator createIterator() {
return new NullIterator();
}
- CompositeIterator
import java.util.Iterator;
import java.util.Stack;
public class CompositeIterator implements Iterator {
Stack stack = new Stack();
/**
* 我们将要遍历的顶层组合的迭代器传入
* 我们把它抛进一个堆栈数据结构中
*
* @param iterator
*/
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
@Override
public boolean hasNext() {
/**
* 想要知道是否还有下一个元素,我们检查堆栈是否被清空
* 如果已经空了,就表示没有下一个元素了
*/
if (stack.empty()) {
return false;
} else {
/**
* 否则我们从栈顶元素中取出迭代器
* 看看是否还有下一个元素
* 如果他没有元素,我们将它弹出堆栈,然后递归调用next()
*/
Iterator iterator = (Iterator) stack.peek();//stack.peek()和stack.popo()均是从栈顶取值,不同的是pop会删除栈顶元素,而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(((Menu) component).createIterator());
}
return component;
} else return null;
}
/**
* 此处设置为不允许删除
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
- NullIterator
import java.util.Iterator;
public class NullIterator implements Iterator {
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
return null;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
下面我解释一下为何要返回NullIterator
9.16.1 空迭代器
到底什么是空迭代器(NullIterator)呢?这么说好了:菜单项内没什么可以遍历的,对吧?那么我们要如何实现菜单项的createIterator()方法呢?有两种选择:
- 选择1:返回null
我们可以让createIterator()方法返回null,但是如果这么做,我们的客户代码就需要条件语句来判断返回值是否为null。 - 选择2:返回一个迭代器,而这个迭代器的hasNext()永远返回false。
这似乎是一个更好的方案。我们依然可以返回一个迭代器,客户不用再担心返回值是否为null。我们等于是创建了一个迭代器,其作用是“没作用”。
9.16.2 素食菜单
我们只需要在女招待里加一个可以确切地告诉我们哪些项目是素食的方法
import java.util.Iterator;
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
/**
* 打印蔬菜类的菜单
*/
public void printVegetraianMenu() {
Iterator iterator = allMenus.createIterator();
System.out.println("\n蔬菜类菜单\n----");
while (iterator.hasNext()) {//遍历组合内的每一个元素
MenuComponent menuComponent = (MenuComponent) iterator.next();
try {
if (menuComponent.isVegetarian()) menuComponent.print();
} catch (UnsupportedOperationException e) {//我们在菜单(Menu上并没有覆盖父类的isVegetain()方法,所以永远都会抛出异常)
}
}
}
}
运行结果如下:
至此迭代器与组合模式我们就到此结束了,顺便帮大家回顾一下之前所学习的一些知识。
9.17 设计原则
9.18 要点
- 迭代器允作访问聚合的元素,而不需要暴露它的内部结构。
- 迭代器将遍历聚合的工作封装进一个对象中。
- 当使用迭代器的时候,我们依赖聚合提供遍历。
- 迭代器提供了一个通用的接口,让我们遍历聚合的项,当我们编码使用聚合的项时,就可以使用多态机制。
- 我们应该努力让一个类只分配一个责任。
- 组合模式提供一个结构,可同时包容个别对象和组合对象。
- 组合模式允许客户对个别对象以及组合对象一视同仁。
- 组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点。
- 在实现组合模式时,有许多设计上的折衷。你要根据需要平衡透明性和安全性。