9 迭代器与组合模式

1.引入

1.1 问题引入

对象村餐厅和对象村煎饼屋合并了。可以在同一个地方享用煎饼屋的煎饼早餐和餐厅午餐。

Lou Mel

检查菜单项

 

MenuItem类的实现:

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;
	}
  
	public String getName() {
		return name;
	}
  
	public String getDescription() {
		return description;
	}
  
	public double getPrice() {
		return price;
	}
  
	public boolean isVegetarian() {
		return vegetarian;
	}
	@Override
	public String toString() {
		return (name + ", $" + price + "\n   " + description);
	}
}

来看Lou和Mel的菜单实现

Lou和Mel的菜单项代码并不统一,并且在菜单的存储方式上花了很多的时间和代码。

煎饼屋Lou用ArrayList来存储菜单

public class PancakeHouseMenu implements Menu {
	ArrayList menuItems;
 
	public PancakeHouseMenu() {
		menuItems = new ArrayList();
    
		addItem("K&B's Pancake Breakfast", 
			"Pancakes with scrambled eggs and toast", 
			true,
			2.99);
 
		addItem("Regular Pancake Breakfast", 
			"Pancakes with fried eggs, sausage", 
			false,
			2.99);
 
		addItem("Blueberry Pancakes",
			"Pancakes made with fresh blueberries",
			true,
			3.49);
 
		addItem("Waffles",
			"Waffles with your choice of blueberries or strawberries",
			true,
			3.59);
	}

	public void addItem(String name, String description,
	                    boolean vegetarian, double price)
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.add(menuItem);
	}
 
	public ArrayList getMenuItems() {
		return menuItems;
	}
  

	//还有菜单的其它方法
}

午餐厅Mel用数组来存储菜单

public class DinerMenu implements Menu {
	static final int MAX_ITEMS = 6;
	int numberOfItems = 0;
	MenuItem[] menuItems;
  
	public DinerMenu() {
		menuItems = new MenuItem[MAX_ITEMS];
 
		addItem("Vegetarian BLT",
			"(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
		addItem("BLT",
			"Bacon with lettuce & tomato on whole wheat", false, 2.99);
		addItem("Soup of the day",
			"Soup of the day, with a side of potato salad", false, 3.29);
		addItem("Hotdog",
			"A hot dog, with sauerkraut, relish, onions, topped with cheese",
			false, 3.05);
		addItem("Steamed Veggies and Brown Rice",
			"Steamed vegetables over brown rice", true, 3.99);
		addItem("Pasta",
			"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
			true, 3.89);
	}
  
	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;
		}
	}
 
	public MenuItem[] getMenuItems() {
		return menuItems;
	}
  
	// 还有菜单的其它方法
}

将这两家餐厅合并之后,需要同时使用这两个菜单的代码,那么就需要打印这些菜单上的内容,打印要求为:

1.2 初步实现需求

1.打印每份菜单上的所有项,必须调用PanckaeHouseMenu和DinerMenu的getMenuItem()方法,来取得他们各自的菜单项。但,这两个方法的返回类型是不同的。

2.现在要打印 PanckaeHouseMenu 的项,我们用循环将早餐ArrayList内的项一一列出来。想要打印DinerMenu的项目,用循环将数组内的项一一列出来。

3.如果采用这样的方法去实现问题需求中的其他问题,那么每次都需要处理两个菜单,并且用两个循环来遍历这些项。如果还有第三家以不同的实现出现,那么就需要有三个循环。这就太麻烦了。

1.3 改进实现需求

从书中学到一件事情,就是封装变化的部分。在此处发生变化的是:由不同的集合(collection)类型所造成的遍历。

1.要遍历早餐项,需要使用ArrayList的size()和get()方法

2.要遍历午餐向,需要使用数组的length字段和中括号

3.现在创建一个对象,将他称之为迭代器(Iterator),利用它来封装“遍历集合内的每个对象的过程”。

先在ArrayList上试试:

 4.在数组上试试: 

 2.会见迭代器模式

2.1 迭代器模式样式

通过对遍历的封装已经有效了,这就是迭代器模式。

关于迭代器模式,需要知道的是,它依赖于一个名为迭代器的接口。下图为一个可能的迭代器的接口:

  •  hasNext(): 该方法告诉我们是否在这个聚合中还有更多的元素
  • next():返回这个聚合中的下一个对象。

现在,一旦有这个接口,就可以为各种对象集合实现迭代器:数组、列表、散列表。。。如果我们想要为数组实现迭代器,以便使用在DinerMenu中。

 当我们说“集合”(collection)的时候,我们指的是一群对象。其存储方式可以是各式各样的数据结构,例如:列表、数组、散列表,无论用什么方式存储,一律可以视为是集合,有时候也被称为聚合(aggregate) 。

 2.2 制造一个迭代器

将迭代器挂钩到DinerMenu中,看其工作的原理。

1.首先创建一个迭代器接口

public interface Iterator {
	boolean hasNext();
	MenuItem next();
}

hasNext():返回一个布尔值,告知是否还有更多的元素

next():返回下一个元素

2.实现一个具体的迭代器,为餐厅菜单服务

public class DinerMenuIterator implements Iterator {
	MenuItem[] items;
	int position = 0;//position 记录当前数组遍历的位置
 
	public DinerMenuIterator(MenuItem[] items) {
		//构造器传入一个菜单项的数组当做参数
		this.items = items;
	}

	//next()方法返回数组内的下一项,并递增其位置
	@Override
	public MenuItem next() {
		/*
		MenuItem menuItem = items[position];
		position = position + 1;
		return menuItem;
		*/
		
		// or shorten to 
		return items[position++];
	}

	//hasNext()方法会检查是否已经取得数组内所有的元素,如果还有元素待遍历,就返回true
	@Override
    public boolean hasNext() {
		/*
		//因为使用的是固定长度的数组,所以不但要检查是否超出了数组长度,还必须要检查是否下一项为null
		//如果是Null,就表示没有其他项了
		if (position >= items.length || items[position] == null) {
			return false;
		} else {
			return true;
		}
		*/
		
		// or shorten to
		return items.length > position;
	}
}


3.用迭代器改写餐厅菜单

已经有了迭代器,现在就利用其来改写餐厅菜单,只需要加入一个方法创建DinerMenuIterator,并将其返回给客户: 



public class DinerMenu {
	static final int MAX_ITEMS = 6;
	int numberOfItems = 0;
	MenuItem[] menuItems;
  
    public DinerMenu() {...}
  
	public void addItem(String name, String description, 
	                     boolean vegetarian, double price) {...}

	//不再需要getMenuItems()方法,因为该方法会暴露内部的实现
    //public MenuItem[] getMenuItems() {
	//	return menuItems;
	//}
  
    
	
	public Iterator createIterator() {
		return new DinerMenuIterator(menuItems);
	}
 
	// other menu methods here
}

getMenuItems()方法,会暴露内部的实现,因此不再使用 

createIterator()方法,用来从菜单项数组创建一个DinerMenuIterator,并将它返回给客户。返回迭代器接口。客户不需要知道餐厅菜单是如何维护菜单项的,也不需要知道迭代器是如何实现的。客户只需要直接使用这个迭代器遍历菜单项即可

4.实现煎饼店菜单项的迭代器和煎饼单菜单改造

煎饼店菜单背后使用ArrayList来实现的,代码为:

//煎饼店的菜单迭代器代码
public class PancakeHouseMenuIterator implements Iterator {
	List<MenuItem> items;
	int position = 0;//用于记录当前访问的位置
 
	public PancakeHouseMenuIterator(List<MenuItem> items) {
		this.items = items;
	}
 
	@Override
	public MenuItem next() {
		/* 
		MenuItem item = items.get(position);
		position = position + 1;
		return item;
		*/
		// or shorten to:
		return items.get(position++);
	}
 
	@Override
    public boolean hasNext() {
		/*
		if (position >= items.size()) {
			return false;
		} else {
			return true;
		}
		*/
		// or shorten to:
		return items.size() > position;
	}
}


//煎饼店菜单
public class PancakeHouseMenu  {
	List<MenuItem> menuItems;
 
	public PancakeHouseMenu() {...}

	public void addItem(String name, String description,
	                    boolean vegetarian, double price)
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.add(menuItem);
	}
 
	public List<MenuItem> getMenuItems() {
		return menuItems;
	}
  
	public Iterator createIterator() {
		return new PancakeHouseMenuIterator(menuItems);
	}
  

	// other menu methods here
}

5.将迭代器代码整合到上面的需求中,来实现各种形式的遍历

整合的方法十分直接,首先创建一个printMenu()方法,传入一个迭代器当做此方法的参数,然后对每一个菜单都使用createIterator()方法来检索迭代器,并将迭代器传入新方法。

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("MENU\n----\nBREAKFAST");
        //对每个迭代器调用重载的(overloaded)printMenu(),将迭代器传入
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);

	}
 
    //重载的printMenu()方法,使用迭代器,来遍历菜单项并打印出来
	private void printMenu(Iterator iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			System.out.print(menuItem.getName() + ", ");
			System.out.print(menuItem.getPrice() + " -- ");
			System.out.println(menuItem.getDescription());
		}
	}
 

//其他方法



}

6.测试代码

public class MenuTestDrive {
	public static void main(String args[]) {
        Menu pancakeHouseMenu = new PancakeHouseMenu();
        Menu dinerMenu = new DinerMenu();
 
		Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
		
		// With iterators
		waitress.printMenu();

	}
}

输出:

8.总结

迭代器让女招待Waitress能够从具体类的实现中解耦。因为两个菜单都使用的Menu的接口,因此不需要知道菜单具体使用的是数组,ArrayList还是其他集合来实现的。只需要去的迭代器,然后进行迭代器遍历即可。

迭代器让我们能够遍历集合中的每个元素,而不会去强迫集合必须提供方法,让我们在他的元素中游走。也可以在集合的外面实现迭代器,即封装了遍历

在Waitress类中发现,这两个菜单都实现了一样的方法,但是没有实现相同的接口,在下面将要修改这一点,使Waitress类不会依赖具体的菜单

2.3 使用java.util.Iterator

首先,看看Java.util.Iterator接口:

 关于remove()方法

问:如果我不想让客户具备删除的能力,该怎么办?
:remove()方法其实是可有可无的、不一定要提供删除的功能。但是,很明显的,你需要提供这样的方法、因为毕竟它被声明在lterator接口中。如果你不允许remove()的话、可以抛出一个java.lang.UnsupportedOperationException运行时异常。lterator的APl文件提到可以让remove()抛出这样的异常,而任何良好的客户程序只要调用了remove()方法,就应该检查是否会发生这个异常。

问:在多线程的情况下,可能会有多个迭代器引用同一个对象集合。remove()会造成怎样的影响?
答:有一个fail-fast(快速失败)机制,可以避免该问题的发生。

利用java.util.Iterator来清理

先从煎饼屋菜单开始,将其改用Java.util.Iterator,因为煎饼屋菜单PancakeHouseMenu本来就是用的ArrayList,这个类里面本来使用了Iteraotr接口,因此只需要将煎饼屋菜单的代码前面导入这个包,然后改变下面的一行代码即可:

	@Override
	public Iterator<MenuItem> createIterator() {
		
		return menuItems.iterator();
	}

这样PancakeHouseMenu类就完成了。

接着来处理DinnerMenu,以符合java.util.Iterator的要求。

 需要对DinnerMenuIterator进行改进,这个是通过数组进行存储的,该类接入java.util.Iterator,并重写几个方法,使之对数组也实现迭代器java.util.Iterator


 import java.util.Iterator;
  
public class DinerMenuIterator implements Iterator<MenuItem> {
	MenuItem[] list;
	int position = 0;
 
	public DinerMenuIterator(MenuItem[] list) {
		this.list = list;
	}
 
    //继承java.util.Iterator接口后,需要重写该接口的三个方法
	@Override
	public MenuItem next() {
		MenuItem menuItem = list[position];
		position = position + 1;
		return menuItem;
	}
 
	@Override
	public boolean hasNext() {
		if (position >= list.length || list[position] == null) {
			return false;
		} else {
			return true;
		}
	}
 
	@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 (list[position-1] != null) {
            //采用移位覆盖的方式进行删除,该后面所有位置的元素全部前移一个位置
			for (int i = position-1; i < (list.length-1); i++) {
				list[i] = list[i+1];
			}
			list[list.length-1] = null;
		}
	}

}

最后,需要给菜单一个共同的接口Menu。这个Menu接口很简单,在里面加一些方法,比如addItem()。

public interface Menu {
	public Iterator createIterator();
}

需要给菜单一个公共的接口Menu,因为可能还需要在里面多加入一些方法,例如addItem()。煎饼屋菜单和餐厅菜单的类,都实现Menu接口,那么下面的女招待就可以利用接口而不是具体类,来引用每一个菜单对象。通过“针对接口编程,而不是实现编程”,就可以减少女招待和具体类之间的依赖。

package designMode.iteratorPatternIteratorR;

import java.util.Iterator;
import java.util.ArrayList;
 
public class Waitress {
	Menu pancakeHouseMenu;
	Menu dinerMenu;
 
	public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
	}
	
	// --- added 12/30/2016 - not in original code
	public void printMenu(int withNewConstructs) {
		ArrayList<MenuItem> breakfastItems = ((PancakeHouseMenu) pancakeHouseMenu).getMenuItems();
		//pMenu.forEach(m -> printMenuItem(m));
		for (MenuItem m : breakfastItems) {
			printMenuItem(m);
		}
		
		MenuItem[] lunchItems = ((DinerMenu) dinerMenu).getMenuItems();
		for (MenuItem m : lunchItems) {
			printMenuItem(m);
		}
	}
	
	public void printMenuItem(MenuItem menuItem) {
		System.out.print(menuItem.getName() + ", ");
		System.out.print(menuItem.getPrice() + " -- ");
		System.out.println(menuItem.getDescription());
	}
	// ---
 
	public void printMenu() {
		Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
		Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();

		System.out.println("MENU\n----\nBREAKFAST");
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);
	}
 
	private void printMenu(Iterator<MenuItem> iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			System.out.print(menuItem.getName() + ", ");
			System.out.print(menuItem.getPrice() + " -- ");
			System.out.println(menuItem.getDescription());
		}
	}
 
    //...

}

更新之后,那么类图:

 3.定义迭代器模式

3.1 迭代器模式的定义

迭代器模式提供一种方法顺序访问一个集合对象中的各个元素,而又不暴露其内部的表示

迭代器模式让我们能够游走在集合内的每一个元素,而又不暴露其内部的表示

把游走("遍历")的任务放在迭代器上,而不是集合上,这样就监护了集合的接口和实现,让责任各得其所。

3.2 迭代器模式的意义

  1. 这个模式给你提供了一种方法,可以顺序访问一个聚集对象中的元素,而又不用知道内部是如何表示的。你已经在前面的两个菜单实现中看到了这一点。在设计中使用迭代器的影响是明显的:如果你有一个统一的方法访问聚合中的每一个对象,你就可以编写多态的代码和这些聚合搭配,使用―—如同前面的printMenu()方法一样,只要有了迭代器,这个方法根本不管菜单项究竟是由数组还是由ArrayList(或者其他能创建迭代器的东西)来保存的
  2. 另一个对你的设计造成重要影响的,是迭代器模式把在元素之间游走的责任交给迭代器,而不是聚合对象。这不仅让聚合的接口和实现变得更简洁,也可以让聚合更专注在它所应该专注的事情上面(也就是管理对象集合),而不必去理会遍历的事情

类图为:

问:我听说过“内部的”迭代器和“外部的”迭代器。这是什么?我们在前面例子中实现的是哪一种?
答:我们实现的是外部的迭代器,也就是说,客户通过调用next()取得下一个元素。而内部的迭代器则是由迭代器自己控制。在这种情况下,因为是由迭代器自行在元素之间游走,所以你必须告诉迭代器在游走的过程中,要做些什么事情,也就是说、你·必须将操作传入给迭代器。因为客户无法控制遍历的过程,所以内部迭代器比外部迭代器更没有弹性。然而,某些人可能认为内部的迭代器比较容易使用、因为只需将操作告诉它,它就会帮你做完所有事情。

问:迭代器可以被实现成向后移动吗,就像向前移动一样?
答:可以。在这样的情况下、你可能要加上两个方法,一个方法取得前一个元素,而另一个方法告诉你是否已经到了集合的最前端。Java的Collection Framework提供另一种迭代器接口,称为ListIterator.这个迭代器在标准的迭代器接口上多加了一个previous()和一些其他的方法。任何实现了List接口的集合,都支持这样的做法。


问:对于散列表这样的集合,元素之间并没有明显的次序关系,我们该怎么办?
答:迭代器意味着没有次序只是取出所有的元素、并不表示取出元素的先后就代表元素的大小次序。对于迭代器来说,数据结构可以是有次序的、或是没有次序的,甚至数据可以是重复的。除非某个集合的文件有特别说明,否则不可以对迭代器所取出的元素大小顺序作出假设。


问:我看到Java有一个Enumeration(枚举)接口;它实现了迭代器模式吗?
答:我们曾经在适配器的那一章中提到过这个接口,还记得吗? java.util.Enumeration是一个有次序的迭代器实现,它有两个方法,hasMoreElements()类似hasNext(),而nextElement()类似next()。然而,你应该比较想使用迭代器,而不是枚举,因为大多数的Java类都支持迭代器。如果你想把这两者互相转换,请复习适配器那一章,在那一章里你实现了枚举和迭代器的适配器。
 

4.设计原则:单一责任

如果我们允许我们的聚合实现它们内部的集合,以及相关的操作和遍历的方法,又会如何?我们已经知道这会增加聚合中的方法个数,但又怎样呢?为什么这么做不好?

:当我们允许一个类不但要完成自己的事情(管理某种聚合),还同时要担负更多的责任(例如遍历)时,我们就给了这个类两个变化的原因。

  1. 如果这个集合改变的话,这个类也必须改变;
  2. 如果遍历的方式进行改变,那么这个类也必须跟着改变。

因此,“改变”又成了设计原则的中心
 

设计原则:一个类应该只有一个引起变化的原因。

要尽量避免类内的改变,因为修改代码很容易造成许多潜在的错误。如果有一个类具有两个改变的原因,那么这会使得将来该类的变化几率上升,当它真的改变时,设计中同时有两个方面将会受到影响。

类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。
这个原则告诉我们.量让每个类保持单一责任

关于术语“内聚”的定义:内聚 (cohesion)这个术语你应该听过,它用来度量一个类或模块紧密地达到单一目的或责任。
当一个模块或一个类被设计成只支持一组相关的功能时,我们说它具有高内聚;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。
内聚是一个比单一责任原则更普遍的概念,但两者其实关系是很密切的。遵守这个原则的类容易具有很高的凝聚力,而且比背负许多责任的低内聚类更容易维护。

5.新增咖啡厅合并

5.1 需求说明

现在将咖啡厅也并购进来,来供应晚餐。

咖啡厅菜单是:

public class CafeMenu{
    // key:String 就是MenuItem中的name属性
    // value: MenuItem
	HashMap<String, MenuItem> menuItems = new HashMap<String, MenuItem>();
  

	public CafeMenu() {
		addItem("Veggie Burger and Air Fries",
			"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
			true, 3.99);
		addItem("Soup of the day",
			"A cup of the soup of the day, with a side salad",
			false, 3.69);
		addItem("Burrito",
			"A large burrito, with whole pinto beans, salsa, guacamole",
			true, 4.29);
	}
 
	public void addItem(String name, String description, 
	                     boolean vegetarian, double price) 
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.put(name, menuItem);
	}
 
	public HashMap<String, MenuItem> getItems() {
		return menuItems;
	}
  
}

5.2  重做咖啡厅代码

将咖啡厅菜单整合进框架是很容易的。因为HashMap本来就有java内置的迭代器:

import java.util.*;

public class CafeMenu implements Menu {

	HashMap<String, MenuItem> menuItems = new HashMap<String, MenuItem>();
  
	public CafeMenu() {...}
 
	public void addItem(String name, String description, 
	                     boolean vegetarian, double price) 
	{
		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
		menuItems.put(name, menuItem);
	}

     //将遍历的事情,交给迭代器,保证单一原则
	//public Map<String, MenuItem> getItems() {
	//	return menuItems;
	//}
  
   
	@Override
	public Iterator<MenuItem> createIterator() {
        //因为value 才是MenuItem类型的,因此要获取 HashMap的value的迭代器
		return menuItems.values().iterator();
	}
}

5.3 测试新的代码--接入咖啡厅菜单

将咖啡厅菜单接入女招待的代码:



import java.util.Iterator;
  
public class Waitress {
	Menu pancakeHouseMenu;
	Menu dinerMenu;
	Menu cafeMenu;
 
	public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
		this.pancakeHouseMenu = pancakeHouseMenu;
		this.dinerMenu = dinerMenu;
		this.cafeMenu = cafeMenu;
	}
 
	public void printMenu() {
		Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
		Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
		Iterator<MenuItem> cafeIterator = cafeMenu.createIterator();

		System.out.println("MENU\n----\nBREAKFAST");
		printMenu(pancakeIterator);
		System.out.println("\nLUNCH");
		printMenu(dinerIterator);
		System.out.println("\nDINNER");
		printMenu(cafeIterator);
	}
 
	private void printMenu(Iterator<MenuItem> iterator) {
		while (iterator.hasNext()) {
			MenuItem menuItem = iterator.next();
			System.out.print(menuItem.getName() + ", ");
			System.out.print(menuItem.getPrice() + " -- ");
			System.out.println(menuItem.getDescription());
		}
	}
 
	
}

 5.4 测试代码

public class MenuTestDrive {
	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();
		
	}
}

输出:

5.5 设计的好处

这么写代码,让女招待从实现中解耦了。因为,女招待是通过迭代器来进行遍历的,这样就不需要担心究竟使用的是哪个实现,因为他使用的都是相同的接口---也就是迭代器的接口---来遍历菜单项。

通过赋予女招待一个选代器,我们好她从菜单项的实现中解耦了.所以今后我们可以轻易地增加新的菜单。

这对女招待比较好,因为现在她能够使用相同代码来遍历任意组内的对象。这也对我们(开发者)比较好,因为我们不用再将实现的细节暴露出来。

5.6 迭代器与集合

我们所使用的这些类都属于Java Collection Framework的一部分。这里所谓的“framework”(框架)指的是一群类和接口,其中包括了ArrayList、Vector、LinkedList、Stack和PriorityQueue。这些类都实现了java.util.Collection接口。这个接口包含了许多有用的方法,可以操纵一群对象。

6.组合模式引入

6.1 问题引入

在女招待Waitress上,Public 方法printMenu()里面调用了三次private 的 printMenu()方法,看来实在有点丑。

每次一有新菜单加入,就必须打开女招待实现并加入更多的代码。这算是违反了“开放-关闭”原则

这不是女招待的错。对于将她从菜单的实现上解耦和提取遍历动作到迭代器,我们都做的很好。但我们仍然将菜单处理成分离而独立的对象------我们需要一种一起管理他们的方法。 

6.2 问题初步尝试解决

我们所要做的事,就是将这些菜单全都打包进一个ArrayList,然后取得它的迭代器,遍历每个菜单。这么一来,女招待的代码就变得很简单,而且菜单再多也不怕了。

1.根据存储菜单的ArrayList里面取出它的迭代器,然后迭代器取出的项就是菜单项

2.然后根据菜单项,创建菜单项的迭代器

3.最后,利用菜单项的迭代器来遍历菜单项里面的元素。

这样,问题看似得到了解决,但是还存在新的问题。

6.3 新的问题

 新的需求,希望能够增加一份 餐后甜点的“子菜单”。那么现在不仅要支持多个菜单,还要支持菜单中的菜单。

 如果能让甜点菜单变成餐厅菜单集合的一个元素,那该有多好。但是根据现在的实现,根本做不到。

 6.4 分析与改进

该是做决策来改写厨师的实现以符合所有菜单(以及子菜单)的需求的时候了。没错,我们要告诉厨师,重新实现他们的菜单已经是不可避免的了。
事实是,我们已经到达了一个复杂级别,如果现在不重新设计,就无法容纳未来增加的菜单或子菜单等需求。
所以,在我们的新设计中,真正需要些什么呢?

  1. 我们需要某种树形结构,可以容纳菜单、子菜单和菜单项。
  2. 我们需要确定能够在每个菜单的各个项之间游走,而且至少要像现在用迭代器一样方便。
  3. 我们也需要能够更有弹性地在菜单项之间游走。比方说,可能只需要遍历甜点菜单,或者可以遍历餐厅的整个菜单(包括甜点菜单在内)。
     

6.5 定义组合模式

组合模式可以解决这个难题,我们并没有放弃迭代器,它仍然是解决方案中的一部分,然而,管理菜单的问题已经到了一个迭代器无法解决的新维度。因此,可以改用组合模式来实现这个部分。

组合模式的定义:允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性

让我们以菜单为例思考这一切:这个模式能够创建一个树形结构,在同一个结构中处理嵌套菜单和菜单项组。通过将菜单和项放在相同的结构中,我们创建了一个“整体/部分”层次结构,即由菜单和菜单项组成的对象树。但是可以将它视为一个整体,像是一个丰富的大菜单。

一旦有了丰富的大菜单,我们就可以使用这个模式来“统一处理个别对象和组合对象”。这意味着什么?它意味着,如果我们有了一个树形结构的菜单、子菜单和可能还带有菜单项的子菜单,那么任何一个菜单都是一种“组合”因为它既可以包含其他菜单,也可以包含菜单项。个别对象只是菜单项------并未持有其他对象。就像你将看到的,使用一个遵照组合模式的设计,让我们能够写出简单的代码,就能够对整个菜单结构应用相同的操作(例如打印! )。

 组合模式让我们能用树形方式创建对象的结构,书里面包含了组合以及个别的对象。

使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

1. 我们可以任意创建复杂的树。

2.然后,将他们视为一个整体,也可以视为许多部分

3.操作能够应用于整体,也适用于部分

组合模式类图

 

Component:Component为组合中的所有对象定义一个接口,不管是组合还是叶节点。Component可以为add(),remove(),getChild()和它的操作实现一些默认的行为。

Client:客户使用Component接口操作组合中的对象。

Composite:Composite的角色是要定义组件的行为,而这样的组件具有子节点。这个Composite也实现了叶节点相关的操作。其中有一些操作可能对于Composite意义不大,因此在这个情况下可能会产生异常。

Leaf:叶节点没有孩子。叶节点通过实现Composite支持的操作,定义了组合内元素的行为。

问:组件、组合、树?我被搞混了。
答:组合包含组件。组件有两种:组合与叶节点元素。听起来像递归是不是?组合持有一群孩子,这些孩子可以是别的组合或者叶节点元素。当用这种方式组织数据的时候,最终会得到树形结构(正确的说法是由上而下的树形结构),根部是一个组合,而组合的分支逐渐往下延伸,直到叶节点为止。

7.利用组合设计菜单

7.1 分析

如何在菜单上应用组合模式呢?

一开始,需要创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。换句话说,我们可以针对菜单或菜单项调用相同的方法。

现在,对于菜单或菜单项来说,有些方法可能不太恰当。但我们可以处理这个问题,等一下就会这么做。至于现在,让我们从头来看看如何让菜单能够符合组合模式的结构:

        Waitress:女招待将使用菜单组件接口访问某菜单和菜单项。

        MenuComponent:菜单组件提供了一个接口,让菜单项和菜单共同使用。因为我们希望能够为这些方法提供默认的实现,所以在这里使用了一个抽象类。这个类中的add()、remove()和getChild()方法都是用来操纵组件的。菜单和菜单项都是组件。

        MenuItem:菜单项覆盖的部分方法,均是对它有意义的方法,对于那些没有意义的方法(例如add()),就置之不理。add()没意义的原因是 菜单项已经是叶节点,它的下面不能再有任何组件。菜单项和菜单都覆盖了print().

        Menu:菜单也覆盖了一些对它有意义的方法,比如增加或删菜单项(或者其他的菜单)。除此之外,我们也使用getName()和setDescription()方法来返回菜单名称与描述。

7.2 实现菜单组件MenuComponent

开始编写菜单组件的抽象类。菜单组件的角色是为叶节点和组合节点提供一个共同的接口。此外,还需要为这些接口提供默认的实现,这样如果菜单项(叶节点)或者菜单(组合)不想实现某些方法的时候(例如,叶节点不想实现getChild()方法),就可以不实现这些方法。



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();
	}
}

“组合”方法为:add(),remove()和getChild(),即新增、删除和取得菜单组件。

“操作”方法为剩下来的方法,他们被菜单项使用,其中有一些也可以用在菜单上。

print()方法是一个“操作”方法,这个方法同时被菜单和菜单项所实现。

因为有些方法对菜单项有意义,而有些则只对莱单有悉义.默认实现是抛出UnsupportedOperationException异常。这样.如果桀荤项或菜单不支持某个操作,他们就不需做任何事情.当接继承默认实现就可以了

7.3 实现菜单项

然后实现菜单项类,这是组合类图里面的叶子类。

package designMode.composite.menu;

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 (isVegetarian()) {
			System.out.print("(v)");
		}
		System.out.println(", " + getPrice());
		System.out.println("     -- " + getDescription());
	}
}

7.4 实现组合菜单

已经有了菜单项,还需要组合类,我们称之为菜单。此组合类可以持有菜单项或者其他菜单。

package designMode.composite.menu;

import java.util.Iterator;
import java.util.ArrayList;

public class Menu extends MenuComponent {
    //菜单可以有任意数目的子项,这些子项的类型必须为MenuComponent
	ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
	String name;
	String description;
  
	public Menu(String name, String description) {
		this.name = name;
		this.description = description;
	}
 
	@Override
	public void add(MenuComponent menuComponent) {
		menuComponents.add(menuComponent);
	}
 
	@Override
	public void remove(MenuComponent menuComponent) {
		menuComponents.remove(menuComponent);
	}
 
	@Override
	public MenuComponent getChild(int i) {
		return (MenuComponent)menuComponents.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 = menuComponents.iterator();
		while (iterator.hasNext()) {
			MenuComponent menuComponent = 
				(MenuComponent)iterator.next();
			menuComponent.print();
		}
	}
}

需要注意的是,对于菜单Menu重写的print()方法,除了将菜单打印出来,还需要将菜单里面包含的菜单项也全部打印出来,因此用到了迭代器,如果菜单里面还包含了子菜单,那么就会进行递归,然后进行打印

7.5 运行时菜单组合

 7.6 测试代码

好了,现在要写一个测试程序。和以前的版本不同,我们这个版本要在测试程序中处理所有菜单的创建。我们可以请每位厨师交出他的新菜单,但是让我们先将这一切测试完毕。代码如下:
 

package designMode.composite.menu;

public class MenuTestDrive {
	public static void main(String args[]) {
		//先创建所有的菜单对象
		MenuComponent pancakeHouseMenu =
			new Menu("PANCAKE HOUSE MENU", "Breakfast");
		MenuComponent dinerMenu = 
			new Menu("DINER MENU", "Lunch");
		MenuComponent cafeMenu = 
			new Menu("CAFE MENU", "Dinner");
		MenuComponent dessertMenu = 
			new Menu("DESSERT MENU", "Dessert of course!");
		MenuComponent coffeeMenu = new Menu("COFFEE MENU", "Stuff to go with your afternoon coffee");

		//需要一个顶层的菜单,将它称之为allMenus
		MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
  
		allMenus.add(pancakeHouseMenu);
		allMenus.add(dinerMenu);
		allMenus.add(cafeMenu);

		//加入菜单项
		pancakeHouseMenu.add(new MenuItem(
			"K&B's Pancake Breakfast", 
			"Pancakes with scrambled eggs and toast", 
			true,
			2.99));
		pancakeHouseMenu.add(new MenuItem(
			"Regular Pancake Breakfast", 
			"Pancakes with fried eggs, sausage", 
			false,
			2.99));
		pancakeHouseMenu.add(new MenuItem(
			"Blueberry Pancakes",
			"Pancakes made with fresh blueberries, and blueberry syrup",
			true,
			3.49));
		pancakeHouseMenu.add(new MenuItem(
			"Waffles",
			"Waffles with your choice of blueberries or strawberries",
			true,
			3.59));

		dinerMenu.add(new MenuItem(
			"Vegetarian BLT",
			"(Fakin') Bacon with lettuce & tomato on whole wheat", 
			true, 
			2.99));
		dinerMenu.add(new MenuItem(
			"BLT",
			"Bacon with lettuce & tomato on whole wheat", 
			false, 
			2.99));
		dinerMenu.add(new MenuItem(
			"Soup of the day",
			"A bowl of the soup of the day, with a side of potato salad", 
			false, 
			3.29));
		dinerMenu.add(new MenuItem(
			"Hot Dog",
			"A hot dog, with saurkraut, relish, onions, topped with cheese",
			false, 
			3.05));
		dinerMenu.add(new MenuItem(
			"Steamed Veggies and Brown Rice",
			"Steamed vegetables over brown rice", 
			true, 
			3.99));
 
		dinerMenu.add(new MenuItem(
			"Pasta",
			"Spaghetti with marinara sauce, and a slice of sourdough bread",
			true, 
			3.89));
   
		dinerMenu.add(dessertMenu);
  		//向子菜单里加入菜单项
		dessertMenu.add(new MenuItem(
			"Apple Pie",
			"Apple pie with a flakey crust, topped with vanilla icecream",
			true,
			1.59));
  
		dessertMenu.add(new MenuItem(
			"Cheesecake",
			"Creamy New York cheesecake, with a chocolate graham crust",
			true,
			1.99));
		dessertMenu.add(new MenuItem(
			"Sorbet",
			"A scoop of raspberry and a scoop of lime",
			true,
			1.89));
		//向子菜单里加入菜单项
		cafeMenu.add(new MenuItem(
			"Veggie Burger and Air Fries",
			"Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
			true, 
			3.99));
		cafeMenu.add(new MenuItem(
			"Soup of the day",
			"A cup of the soup of the day, with a side salad",
			false, 
			3.69));
		cafeMenu.add(new MenuItem(
			"Burrito",
			"A large burrito, with whole pinto beans, salsa, guacamole",
			true, 
			4.29));

		cafeMenu.add(coffeeMenu);

		coffeeMenu.add(new MenuItem(
			"Coffee Cake",
			"Crumbly cake topped with cinnamon and walnuts",
			true,
			1.59));
		coffeeMenu.add(new MenuItem(
			"Bagel",
			"Flavors include sesame, poppyseed, cinnamon raisin, pumpkin",
			false,
			0.69));
		coffeeMenu.add(new MenuItem(
			"Biscotti",
			"Three almond or hazelnut biscotti cookies",
			true,
			0.89));
 
		Waitress waitress = new Waitress(allMenus);
   
		waitress.printMenu();
	}
}

输出:

 7.7 实现组合迭代器

7.7.1 框架引入

已经在print()方法内部的实现中使用了迭代器,除此之外,如果女招待需要,我们也能让她使用迭代器遍历整个组合。比方说,女招待可能想要游走整个菜单,挑出素食项。
想要实现一个组合迭代器,需要为每个组件都加上createIterator()方法。从抽象的MenuComponent类下手:

在MenuComponent中加入一个createIterator()方法。这意味着,每个菜单和菜单项都必须实现这个方法。也意味着,对一个组合调用createIterator()方法,将会应用于该组合的所有孩子。

现在需要在菜单和菜单项类中实现这个方法:

public class Menu extends MenuComponent {
  
    //其他部分的代码不需要修改

	@Override
	public Iterator<MenuComponent> createIterator() {
		return new CompositeIterator(menuComponents.iterator());;
	}

}

此处使用一个新的,被称为CompositeIterator的迭代器。这个迭代器知道如何遍历任何组合。我们将目前组合的迭代器传入他的构造器。

public class MenuItem extends MenuComponent {
 
    //其他部分的代码不需要修改

	@Override
	public Iterator<MenuComponent> createIterator() {
		return new NullIterator();
	}
 
}

菜单项不需要迭代器,因此返回的是NullIterator.

7.7.2 组合迭代器实现

这个CompositeIterator是一个不可小觑的迭代器。它的工作是遍历组件内的菜单项,并且确保所有的子菜单都被包括进来

代码如下:

import java.util.*;
  
public class CompositeIterator implements Iterator<MenuComponent> {
	//将需要遍历的顶层组合的迭代器传入。我们将它抛进一个堆栈数据结构中。
	Stack<Iterator<MenuComponent>> stack = new Stack<Iterator<MenuComponent>>();
   
	public CompositeIterator(Iterator<MenuComponent> iterator) {
		stack.push(iterator);
	}
   
	@Override
	public MenuComponent next() {
		if (hasNext()) {//通过hasNext()来确定是否还有下一个元素
            //如果还有下一个元素,我们就从堆栈中取出目前的迭代器,然后取得它的下一个元素
			Iterator<MenuComponent> iterator = stack.peek();
			MenuComponent component = iterator.next();
            //如果元素是一个菜单,我们就有了另一个需要被包含进遍历中的组合,因此将它丢进对战中
            if(component instanceof Menu){
			    stack.push(component.createIterator());
            }
            //不管是不是菜单,我们都返回该组件
			return component;
		} else {
			return null;
		}
	}
  
	@Override
	public boolean hasNext() {
		//想要知道是否还有下一个元素,我们检查堆栈是否被清空;如果已经空了,就表示没有下一个元素了
		if (stack.empty()) {
			return false;
		}
		else {
		//否则,我们就从堆栈的顶层中取出迭代器,看看是否还有下一个元素。
			Iterator<MenuComponent> iterator = stack.peek();//peek()是返回顶部元素,但不删除
			if (!iterator.hasNext()) {// 如果迭代器没有元素,我们将它弹出堆栈,然后递归调用hasNext()
				stack.pop();//pop()是弹出顶部元素,相当于删除
				return hasNext();
			} else {
				return true;
			}
		}
	}
	

	//不支持删除,这里只有遍历
	@Override
	public void remove() {
		throw new UnsupportedOperationException();
	}

}





在我们写MenuComponent类的print()方法的时候,我们利用了一个迭代器来遍历组件内的每个项。如果遇到的是菜单(而不是菜单项),我们就会递归地调用print()方法处理它。换句话说,MenuComponent是在“内部”自行处理遍历。
但是在上页的代码中,我们实现的是一个“外部”的迭代器,所以有许多需要追踪的事情。外部迭代器必须维护它在遍历中的位置,以便外部客户可以通过调用hasNext()和next()来驱动遍历。在这个例子中,我们的代码也必须维护组合递归结构的位置。这也就是为什么当我们在组合层次结构中上上下下时,使用堆栈来维护我们的位置。
 

7.7.3 空迭代器

为什么要使用空迭代器(NullIterator)?

菜单项没什么可以遍历的。那么如何实现菜单项的createIterator()方法呢?有两种选择:

选择一:返回Null

可以让createIteraotr()方法返回null,但是如果这么做,我们的客户代码就需要条件语句来判断返回值是否为Null.

选择二:返回一个迭代器,而这个迭代器的hasNext()永远返回false

这是一个更好的方案。因为依然返回一个迭代器,客户不用在担心返回值是否为Null,这就等于创建了一个迭代器,其作用是“没作用”。

空迭代器NullIterator的作用:


import java.util.Iterator;
  
public class NullIterator implements Iterator<MenuComponent> {
    

    //当next()被调用时,返回Null   
	@Override
	public MenuComponent next() {
		return null;
	}
  
    //hasNext()被调用时,永远返回Null
	@Override
	public boolean hasNext() {
		return false;
	}
   

	//空迭代器不支持remove
	@Override
	public void remove() {
		throw new UnsupportedOperationException();
	}

}

7.7.4 素食菜单

为女招待加上一个可以告诉那些项目是素食的方法:

public class Waitress {
	MenuComponent allMenus;
 
	public Waitress(MenuComponent allMenus) {
		this.allMenus = allMenus;
	}
 
	public void printMenu() {
		allMenus.print();
	}
  
	public void printVegetarianMenu() {
		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) {}
		}
	}
}

8.总结

  1. 迭代器允许访问聚合的元素,而不需要暴露它的内部结构。
  2. 迭代器将遍历聚合的工作封装进一个对象中。
  3. 当使用迭代器的时候,我们依赖聚合提供遍历。
  4. 迭代器提供了一个通用的接口,让我们遍历聚合的项,当我们编码使用聚合的项时,就可以使用多态机制。
  5. 我们应该努力让一个类只分配一个责任。
  6. 组合模式提供一个结构,可同时包容个别对象和组合对象。
  7. 组合模式允许名户对个别对象以及组合对象一视同仁。
  8. 组合结构内的任意对象为组件。组件可以是组合.也可以是叶节点。
  9. 在实现组合模式时,有许多设计上的折折衷。你要根据需要平衡透明性和安全性。
     

 REF

关于组合模式,书中的例子再后来改写代码有点难理解,该模式的讲解可以参考博客:JAVA设计模式初探之组合模式_一个本科小生的奋斗史-CSDN博客_java设计模式之组合模式

其他的参考:Java 设计模式——组合模式_大鱼-CSDN博客_组合模式 

一个大佬完整的设计模式目录:史上最全设计模式导学目录(完整版)_刘伟技术博客-CSDN博客_趣学设计模式

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值