什么是迭代器模式
迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。
HeadFirst 上实例练习
MenuItem
新的餐厅想用煎饼屋菜单当作早餐的菜单,使用餐厅的菜单当做午餐的菜单,大家都同意了这样实现菜单项。但是大家无法同意菜单的实现。煎饼屋使用ArrayList记录他的菜单项,而餐厅使用的是数组。他们两个都不愿意改变他们的实现,毕竟有太多代码依赖于它们了。
package iterator.ver1;
/**
* Created by liqiushi on 2017/12/5.
*/
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;
}
}
package iterator.ver1;
import java.util.ArrayList;
/**
* Created by liqiushi on 2017/12/5.
*/
public class PancakeHouseMenu {
// 煎饼屋使用一个ArrayList存储他的菜单项
ArrayList menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
// 在菜单的构造器中,每一个菜单项都会被加入到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);
}
// 要加入一个菜单项,煎饼屋的做法是创建一个新的菜单项对象,
// 传入每一个变量,然后将它加入ArrayList中
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;
}
// 这里还有菜单的其他方法,这些方法都依赖于这个ArrayList,所以煎饼屋不希望重写全部的代码!
// ...
}
最初版本的实现
// getMenuItems()方法看起来是一样的,但是调用所返回的结果却是不一样的类型。
// 早餐项是在一个ArrayList中,午餐项则是在一个数组中
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList breakfastItems = pancakeHouseMenu.getMenuItems();
DinnerMenu dinnerMenu = new DinnerMenu();
MenuItem[] lunchItems = dinnerMenu.getMenuItems();
// 现在,我们必须实现两个不同的循环,个别处理这两个不同的菜单
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = (MenuItem) breakfastItems.get(i);
System.out.print(menuItem.getName() + " ");
System.out.print(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.print(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription() + " ");
}
因为返回的类型不同,所以我们需要两个循环来分别处理。如果我们现在想通过一种方式就可以遍历,而且并不需要知道其中的实现,让他们的两个菜单实现同一个接口,封装变化的部分
迭代器
package iterator.ver1;
/**
* Created by liqiushi on 2017/12/6.
*/
public interface Iterator {
Boolean hasNext();
Object next();
}
迭代器实现
package iterator.ver1;
/**
* Created by liqiushi on 2017/12/6.
*/
public class DinnerMenuIterator implements Iterator {
MenuItem[] items;
// position记录当前数组遍历的位置
int position = 0;
// 构造器需要被传入一个菜单项的数组当做参数
public DinnerMenuIterator(MenuItem[] items) {
this.items = items;
}
// next()方法返回数组内的下一项,并递增其位置
public Object next() {
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
// hasNext()方法会检查我们是否已经取得数组内所有的元素。
// 如果还有元素待遍历,则返回true
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
}
修改之前的菜单
- DinnerMenu
public class DinnerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
// ...
// 我们不再需要getMenuItems()方法,事实上,我们根本不想要这个方法,
// 因为它会暴露我们内部的实现。
// 这是createIterator()方法,用来从菜单项数组创建一个DinnerMenuIterator,
// 并将它返回给客户
public Iterator createIterator() {
return new DinnerMenuIterator(menuItems);
}
// ...
}
加入迭代器后的女服务生
public class Waitress {
PancakeHouseMenu pancakeHouseMenu;
DinnerMenu dinnerMenu;
// 在构造器中,女招待照顾两个菜单
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinnerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}
public void printMenu() {
// 这个printMenu()方法为每一个菜单各自创建一个迭代器
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinnerIterator = dinnerMenu.createIterator();
// 然后调用重载的printMenu(),将迭代器传入
printMenu(pancakeIterator);
printMenu(dinnerIterator);
}
// 这个重载的printMenu()方法,使用迭代器来遍历菜单项并打印出来
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.println(menuItem.getName() + " " +
menuItem.getPrice() + " " + menuItem.getDescription());
}
}
}
继续优化
Menu接口,使女招待员解耦
package iterator.ver2;
import java.util.Iterator;
/**
* Created by liqiushi on 2017/12/7.
*/
public interface Menu {
Iterator createIterator();
}
女招待员
//...
// 在构造器中,女招待照顾两个菜单
public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}
//...
其他的优化
将菜单放在一个ArraList里,遍历它的所有项
或者后面的组合模式,用一个迭代器
迭代器模式的优缺点
定义迭代器模式
现在我们来看看这个模式的正式定义:
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
迭代器模式让我们能游走于聚合内的每一个元素,而又不暴露内部的表示。把游走的任务放在迭代器上,而不是聚合上,这样简化了聚合的接口和实现,也让责任各得其所。
这很有意义:这个模式给你提供了一种方法,可以顺序访问一个聚集对象中的元素,而又不用知道内部是如何表示的。你已经在前面的两个菜单实现中看到了这一点。在设计中使用迭代器的影响是明显的:如果你有一个统一的方法访问聚合中的每一个对象,你就可以编写多态的代码和这些聚合搭配使用,如同前面的printMenu()方法一样,只要有了迭代器这个方法,根本不管菜单项究竟是由数组还是由ArrayList(或者其他能创建迭代器的东西)来保存的。
另一个对你的设计造成重要影响的,是迭代器模式把这些元素之间游走的责任交给迭代器,而不是聚合对象。这不仅让聚合的接口和实现变得更简洁,也可以让聚合更专注在它所应该专注的事情上面(也就是管理对象组合),而不必去理会遍历的事情。
单一责任
如果我们允许我们的聚合实现它们内部的集合,以及相关的操作和遍历的方法,又会如何?我们已经知道这会增加聚合中的方法个数,但又怎样呢?为什么这么做不好?
想知道为什么,首先你需要认清楚,当我们允许一个类不但要完成自己的事情(管理某种聚合),还同时要担负更多的责任(例如遍历)时,我们就给了这个类两个变化的原因。两个?没错,就是两个!如果这个集合改变的话,这个类也必须改变,如果我们遍历的方式改变的话,这个类也必须跟着改变。所以,再一次地,我们的老朋友“改变”又成了我们设计原则的中心:
设计原则:一个类应该只有一个引起变化的原因
我们知道要避免类内的改变,因为修改代码很容易造成许多潜在的错误。如果有一个类具有两个改变的原因,那么这会使得将来该类的变化几率上升,而当它真的改变时,你的设计中同时有两个方面将会受到影响。
没错,这听起来很容易,但其实做起来并不简单:区分设计中的责任,是最困难的事情之一。我们的大脑很习惯看着一大群的行为,然后将它们集中在一起,尽管他们可能属于两个或者多个不同的责任。想要成功的唯一方法,就是努力不懈地检查你的设计,随着系统的成长,随时观察有没有迹象显示某个类改变的原因超出一个。