1.本节的一个话题引子是一个餐厅,它提供早餐和午餐,但是在订制菜单的时候,早餐(Pancake)和午餐(Dinner)的实现却造成了一些麻烦。订制早餐的菜单是用ArrayList这样一个数据池来维护的。但是订制午餐的菜单则是一个标准数组进行维护。那么在设计订餐程序来遍历这两个不同的数据结构形成的菜单的时候,就会比较麻烦,毕竟返回的数据类型不是一样的。(不知道我说清楚了没,参阅英文版原书P321)
2.对于这样的迭代场景,我们就可以使用所谓迭代器模式。它依赖于一个名为迭代器的接口,然后让所有相关的部分(在这个例子就是两个菜单的迭代器)实现该迭代器。提供了一种方法顺序访问一个聚合对象的各个元素,同时又不暴露其内部的表示。具体的,我们先加入一个迭代器:
public interface Iterator { boolean hasNext(); Object next(); }
然后我们根据早餐和午餐具体的数据结构来实现这个接口:
public class DinerMenuIterator implements Iterator { MenuItem[] items; 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() { if (position >= items.length || items[position] == null) { return false; } else { return true; } } } public class PancakeHouseMenuIterator implements Iterator { ArrayList items; int position = 0; public PancakeHouseMenuIterator(ArrayList items) { this.items = items; } public Object next() { Object object = items.get(position); position = position + 1; return object; } public boolean hasNext() { if (position >= items.size()) { return false; } else { return true; } } }
通过上述实现接口的模式,我们把游走的任务放在迭代器上,而不是具体聚合上,这样既简化了聚合的接口和实现,也让责任各得其所。对于早餐和午餐的菜单,我们做如下的设计:
早餐:
public class PancakeHouseMenu { 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; } public Iterator createIterator() { return new PancakeHouseMenuIterator(menuItems); } public String toString() { return "Objectville Pancake House Menu"; } }
午餐:
public class DinerMenu { 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 saurkraut, 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; } public Iterator createIterator() { return new DinerMenuIterator(menuItems); } }
注意加粗的部分,我们加入了一个迭代器的工厂方法。此时,我们现在的服务生就可以从容的处理早餐和晚餐了:
public class Waitress { //原书中的第一个例子就是这么写的,而且它自己也指出了可以面向接口Menu编程,进行统一定义,不过不知道为何它在第二个例子中才使用,其实在第一个例子中就直接面向接口将这些定义转化为Menu 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"); printMenu(pancakeIterator); System.out.println("/nLUNCH"); printMenu(dinerIterator); } 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 void printVegetarianMenu() { printVegetarianMenu(pancakeHouseMenu.createIterator()); printVegetarianMenu(dinerMenu.createIterator()); } public boolean isItemVegetarian(String name) { Iterator breakfastIterator = pancakeHouseMenu.createIterator(); if (isVegetarian(name, breakfastIterator)) { return true; } Iterator dinnerIterator = dinerMenu.createIterator(); if (isVegetarian(name, dinnerIterator)) { return true; } return false; } private void printVegetarianMenu(Iterator iterator) { while (iterator.hasNext()) { MenuItem menuItem = (MenuItem)iterator.next(); if (menuItem.isVegetarian()) { System.out.print(menuItem.getName()); System.out.println("/t/t" + menuItem.getPrice()); System.out.println("/t" + menuItem.getDescription()); } } } private boolean isVegetarian(String name, Iterator iterator) { while (iterator.hasNext()) { MenuItem menuItem = (MenuItem)iterator.next(); if (menuItem.getName().equals(name)) { if (menuItem.isVegetarian()) { return true; } } } return false; } }
我们下两个单试一试:
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(); } }
3.这样看上去代码是增加了不少,但是对于两个本来写好的类(PancakeHouseMenu和DinerMenu),我们只在其中加入了createIterator这个方法,系统的耦合性很低。此时女招待这个类就不需要知道菜单具体的实现了,并且实现迭代器后我们只需一个循环就可以遍历了。
4.其实,Java中就有迭代器,我们将PancakeHouseMenu和DinerMenu所扩展的接口换成java.util的迭代器接口即可,连ArrayList也有一个返回一个迭代器的iterator方法。这样可以进一步减少依赖。首先,我们不需要去创建Iterator接口,直接使用JAVA中的迭代器接口来构造DinerMenuIterator:
public class DinerMenuIterator implements Iterator { MenuItem[] list; int position = 0; public DinerMenuIterator(MenuItem[] list) { this.list = list; } public Object next() { MenuItem menuItem = list[position]; position = position + 1; return menuItem; } public boolean hasNext() { if (position >= list.length || list[position] == null) { return false; } else { return true; } } public void remove() {//注意,这个是Util.Iterator中要实现的方法 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; } }
注意:要是不想提供remove方法,那要怎么办呢?我们可以抛出一个UnsupportedOperationException异常表示不支持这样的方法
public class AlternatingDinerMenuIterator implements Iterator { MenuItem[] items; int position; public AlternatingDinerMenuIterator(MenuItem[] items) { this.items = items; Calendar rightNow = Calendar.getInstance(); position = rightNow.DAY_OF_WEEK % 2; } public Object next() { MenuItem menuItem = items[position]; position = position + 2; return menuItem; } public boolean hasNext() { if (position >= items.length || items[position] == null) { return false; } else { return true; } } public void remove() { throw new UnsupportedOperationException( "Alternating Diner Menu Iterator does not support remove()"); } }
而PancakeHouseMenu 则不需要再实现,因为ArrayList直接有返回迭代器的相应方法。
我们现在开始实现菜单,等等!!
我们发现在两个菜单中有一个共同的方法createIterator,那么在这我们又有一个新的改进——可以通过面向接口编程,使二者统一起来:
public interface Menu { public Iterator createIterator(); }
早餐:
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, and blueberry syrup", 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; } public Iterator createIterator() { return menuItems.iterator();//这是我们唯一修改的部分,由于Arraylist直接对应得到其迭代器的方法,所以直接调用该方法即可。 } // other menu methods here }
晚餐:
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 saurkraut, relish, onions, topped with cheese", false, 3.05); addItem("Steamed Veggies and Brown Rice", "A medly of 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; } public Iterator createIterator() { return new DinerMenuIterator(menuItems); //return new AlternatingDinerMenuIterator(menuItems);//这里你可以根据选择使用要不要支持remove 的迭代器 } // other menu methods here }
我们针对这个实现,来创建的女招待代码就变得更加松耦合:
public class Waitress { Menu pancakeHouseMenu; Menu dinerMenu; public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) { this.pancakeHouseMenu = pancakeHouseMenu; this.dinerMenu = dinerMenu; } public void printMenu() { Iterator pancakeIterator = pancakeHouseMenu.createIterator(); Iterator dinerIterator = dinerMenu.createIterator(); System.out.println("MENU/n----/nBREAKFAST"); printMenu(pancakeIterator); System.out.println("/nLUNCH"); printMenu(dinerIterator); } 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 void printVegetarianMenu() { System.out.println("/nVEGETARIAN MENU/n----/nBREAKFAST"); printVegetarianMenu(pancakeHouseMenu.createIterator()); System.out.println("/nLUNCH"); printVegetarianMenu(dinerMenu.createIterator()); } public boolean isItemVegetarian(String name) { Iterator pancakeIterator = pancakeHouseMenu.createIterator(); if (isVegetarian(name, pancakeIterator)) { return true; } Iterator dinerIterator = dinerMenu.createIterator(); if (isVegetarian(name, dinerIterator)) { return true; } return false; } private void printVegetarianMenu(Iterator iterator) { while (iterator.hasNext()) { MenuItem menuItem = (MenuItem)iterator.next(); if (menuItem.isVegetarian()) { System.out.print(menuItem.getName()); System.out.println("/t/t" + menuItem.getPrice()); System.out.println("/t" + menuItem.getDescription()); } } } private boolean isVegetarian(String name, Iterator iterator) { while (iterator.hasNext()) { MenuItem menuItem = (MenuItem)iterator.next(); if (menuItem.getName().equals(name)) { if (menuItem.isVegetarian()) { return true; } } } return false; } }
这样我们通过接口抽象完成了迭代器和菜单的松耦合。但需要注意的是,这个线程不安全。当然,有人会问是不是可以实现向后移动,比如pervious(),那么这时你还必须实现一个方法hasPervious()来验证是不是已经到顶端。Java中有一个ListIterator可以供参考。
5.最后我们思考一个问题:那我们为什么不在这两个聚合(早餐菜单和晚餐菜单)的内部实现迭代器功能的相关方法呢?我们可以分析一下这个问题如果答案是“可以”的话,将会带来什么。若这个集合改变的话,我们要修改这个类;若遍历的方法改变的话,我们要修改这个类——我们至少有两个引起类变化的原因。这会导致代码的重构效率大大降低。
由此我们引出一个设计原则:单一原则——一个类应该只有一个引起变化的原因,类的每个职责都有改变的潜在区域,一旦超过职责就意味着超过“一个改变”的区域。这个原则极难遵守,因为我们的大脑总习惯自然地把很多东西聚在一起。我们经常说“高内聚”,其实真正的含义在于——当一个模块或一个类被设计为只支持一组相关的功能时,我们称其为高内聚;反之,当被设计为支持一组不相关的功能时,我们说其低内聚。