本文对迭代器模式进行了概括和总结
迭代器模式——提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示
前言
有许多种方法可以把对象堆起来成为一个集合(collection),你可以把它们放进数组、堆栈、列表或者是散列表中,这是你的自由。每一种都有它自己的优点和合适的使用时机,但当你的客户想要遍历你的对象时,你打算让客户看到你的实现吗?我们当然希望最好不要。
在本章中你将学习如何能让客户遍历你的对象而又无法窥视你存储对象的方式;也将学习如何创建一些对象超集合,能够一口气跳过某些令人望而生畏的数据结构。还会学习一些关于对象职责的知识。
问题描述
Lou的餐厅要和Mel的煎饼屋合并了,但是Lou是用ArrayList记录菜单项,而Mel用的是数组,他们想合并,但是不想改变他们各自的实现。
现假设Lou和Mel都愿意实现菜单项(MenuItem)接口,这是他们能够合作的基础,
但是Lou的餐厅和Mel的煎饼屋要分工合作,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 boolean isVegetarian() {
return vegetarian;
}
public double getPrice() {
return price;
}
}
不管Lou和Mel如何做自己的事情,他们必须说清楚菜品的name,description,isVegetarian(是否为素菜)和price。
合作的第一天,Lou这样实现他需要完成的早餐菜单:
/*
* Lou 的煎饼屋的实现
* */
public class PancakeHouseMenu_Lou {
ArrayList menuItems;//用的容器是ArrayList
public PancakeHouseMenu_Lou(){
menuItems = new ArrayList();
//制作了三种菜品,并存放在ArrayList中
addItem("K&B's Pancake breakfast","Pancakes with scrambled eggs",true,2.99);
addItem("Regular Pancake breakfast","Pancakes with fried eggs",true,2.99);
addItem("Blueberry Pancake breakfast","Pancakes made with fresh blueberries",true,3.49);
}
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这样实现他的中餐菜单:
/*
* Mel 的餐馆实现
* */
public class DinnerMenu_Mel {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems; //用的容器是数组
public DinnerMenu_Mel(){
menuItems = new MenuItem[MAX_ITEMS];
//制作了两种菜品,存放在数组中
addItem("Vegetarian BLT","Bacon with lettuce,tomato and wheat",true,2.99);
addItem("Hot Dog","a hot with relish and onions",false,3.05);
}
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++;
}
}
public MenuItem[] getMenuItems(){
return menuItems;
}
}
合作第一天,服务员要做下面这些事情:
import java.util.ArrayList;
public class Waiter {
public static void main(String[] args){
PancakeHouseMenu_Lou pancakeHouseMenu = new PancakeHouseMenu_Lou();
ArrayList breakfastItems = pancakeHouseMenu.getMenuItems();//从Lou煎饼屋获取菜单
DinnerMenu_Mel dinnerMenu = new DinnerMenu_Mel();
MenuItem[] lunchItems = dinnerMenu.getMenuItems();//从Mel餐厅获取菜单
//遍历Lou煎饼屋制作的所有早餐,将它们的信息打印出来
for (int i = 0;i < breakfastItems.size();i++){
MenuItem menuItem = (MenuItem) breakfastItems.get(i);
System.out.println(menuItem.getName() + "");
System.out.println(menuItem.getPrice() + "");
System.out.println(menuItem.getDescription());
}
//遍历Mel餐厅制作的所有中餐,将它们的信息打印出来
for (int j = 0;j < lunchItems.length;j++){
MenuItem menuItem = lunchItems[j];
System.out.println(menuItem.getName() + "");
System.out.println(menuItem.getPrice() + "");
System.out.println(menuItem.getDescription());
}
}
}
Mel和Lou让我们很为难。他们都不想改变自身的实现,因为那样意味着要重写许多代码。但是如果他们其中一人不肯退让,我们就很难办了,我们的服务员程序将会难以维护、难以扩展。我们现在需要做的是让他们的菜单实现一个相同的接口!
我们在之前学到的很重要的一点是:封装变化的部分
在现在这个问题上,发生的变化是:由不同的集合类型造成的遍历
为了解决这个问题,迭代器登场了!
迭代器模式依赖于一个名为Iterator的接口,它的关键是hasNext()方法和next()方法。hasNext()方法用于控制循环(就像上面的i < breakfastItems.size()
和j < lunchItems.length
一样)。
next()的作用则是将角标进行移动(就像上面的MenuItem menuItem = (MenuItem) breakfastItems.get(i);i++;
和 MenuItem menuItem = lunchItems[j]; j++;
一样)。
针对不同的容器,可以设计不同的迭代器,它们内部实现hasNext()和next()方法的方式不同,但是不同的迭代器的外观都是相同的,它们都是只有hasNext()和next()方法,并且用法一样。
这样一来,Lou的ArrayList和Mel的数组都可以用迭代器实现,可以统一使用了。
现在来给Lou和Mel各自配备一个迭代器:
Lou的迭代器:
import java.util.ArrayList;
import java.util.Iterator;
//这里用到的是系统的Iterator,因此要将相关的包import进来
//其实也可以用自己定义的Iterator接口
/*像这样
* public interface Iterator{
* boolean hasNext();
* Object next();
* }
*/
public class PancakeHouseMenuIterator implements Iterator {
ArrayList items;
int position;
public PancakeHouseMenuIterator(ArrayList items){
this.items = items;
}
@Override
public boolean hasNext() {
if (position >= items.size() || items.get(position) == null)
return false;
else
return true;
}
@Override
public Object next() {
MenuItem menuItem = (MenuItem) items.get(position);
position ++;
return menuItem;
}
}
Mel的迭代器:
import java.util.Iterator;
public class DinnerMenuIterator implements Iterator {
MenuItem[] items;
int position;
public DinnerMenuIterator(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 ++;
return menuItem;
}
//由于数组的长度是固定的,我们需要调用remove()方法将后面的元素向前移动
@Override
public void remove() {
if (position <= 0){
throw new IllegalStateException("you can't remove a 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;
}
}
}
有了各自的迭代器之后,Lou和Mel的餐厅代码就需要用上迭代器了,下面对两个餐厅的代码进行修正:
用上自己研发的迭代器的Lou的煎饼屋菜单代码:
import java.util.ArrayList;
import java.util.Iterator;
public class PancakeHouseMenuWithIter {
ArrayList menuItems;
public PancakeHouseMenuWithIter(){
menuItems = new ArrayList();
addItem("K&B's Pancake breakfast","Pancakes with scrambled eggs",true,2.99);
addItem("Regular Pancake breakfast","Pancakes with fried eggs",true,2.99);
addItem("Blueberry Pancake breakfast","Pancakes made with fresh blueberries",true,3.49);
}
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 new PancakeHouseMenuIterator(menuItems);
}
}
用上自己研发的迭代器的Mel的餐厅菜单代码:
Mel餐厅菜单代码也同理,只改变了返回容器的那个方法,改成了:
public Iterator createIterator(){
return new DinnerMenuIterator(menuItems);
}
有了迭代器,服务员的代码就很简单了,因为不管是什么迭代器,用法都是一样的,服务员可以随意使用各种迭代器,而不用关心他们的内部实现。
现在更新服务员的代码,这次是waitress :
import java.util.Iterator;
public class Waitress {
public static void main(String[] args){
PancakeHouseMenuWithIter pancakeHouseMenu = new PancakeHouseMenuWithIter();
Iterator breakfastItems = pancakeHouseMenu.createIterator();//从Lou煎饼屋获取菜单
DinnerMenuWithIter dinnerMenu = new DinnerMenuWithIter();
Iterator lunchItems = dinnerMenu.createIterator();//从Mel餐厅获取菜单
//通过迭代器遍历Lou煎饼屋和Mel餐厅制作的所有菜品,将所有菜品信息打印出来
//这样一来,不管是什么容器,只要用迭代器实现,然后将迭代器传入printMenu方法,就能将所有菜品信息打印出来
printMenu(breakfastItems);
printMenu(lunchItems);
}
public static 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());
}
}
}
以上就是迭代器带来的好处:封装了遍历
但是在本例中,还可以进一步改进:
上述有两个不同的具体餐馆类PancakeMenu 和 DinnerMenu ,我们可以将PancakeMenu类 和 DinnerMenu类抽象为一个Menu接口,这样服务员就不用依赖各种不同的具体类了,而只用实现Menu接口就行了。Menu接口的代码如下:
public interface Menu {
public Iterator createIterator();
}
然后让PancakeMenu 和 DinnerMenu都实现Menu接口即可,这样在服务员的代码中,pancakeMenu 和 DinnerMenu对象都可以当作Menu类型处理。
在本文末尾介绍一下本节的设计原则
单一责任原则——一个类应该只有一个引起变化的原因
我们知道要避免类内的改变,因为修改代码很容易造成许多潜在的错误。如果有一个类具有两个改变的原因,那么这会使得将来的该类的变化几率上升,而当它真的改变时,你的设计中同时有两个方面将会受到影响。