迭代函数模式(Iterator)又叫游标模式(Cursor),是对象的行为模式。
迭代函数模式可以顺序的访问一个聚集中的元素而不必暴露聚集的内部表象。
当这里提到聚集时,是泛指包括Java聚集在内的一般性对象集合。
而当提到Java聚集时,则专指实现了 java.util.Collection接口的那些聚集对象。
为什么聚集需要迭代函数
聚集对象必须提供适当的方法,允许客户端能够按照一个线性顺序遍历所有的元素对象,
把元素对象提取出来或者删除掉等。
开闭原则
开闭原则要求系统可以在不修改已有代码的情况下进行功能的扩展,做到这一点的途径就是对变化的封装。
迭代函数模式的结构
角色如下:
- 抽象迭代函数角色(Iterator): 此抽象角色定义出遍历元素所需的接口。
- 具体迭代函数角色(ConcreteIterator): 此角色实现了Iterator接口,并保持迭代过程中的游标位置。
- 聚集角色(Aggregate): 此抽象角色给出创建迭代函数(Iterator)对象的接口。
- 具体聚集角色(ConcreteAggregate): 实现了创建迭代函数(Iterator)对象的接口,返回一个合适的具体迭代函数实例。
- 客户端角色(Client): 持有对聚集及其迭代函数对象的引用,调用迭代函数对象的迭代接口,也有可能通过迭代函数操作聚集元素的增加和删除。
宽接口和窄接口
如果一个聚集的接口提供了可以用来修改聚集元素的方法,这个接口就是所谓的宽接口;
如果一个聚集的接口没有提供修改聚集元素的方法,这样的接口就是所谓的窄接口。
白箱聚集和外禀迭代函数
一个白箱聚集向外界提供访问自己内部元素的接口(称做遍历方法或者Traversing Method),
从而使外禀迭代函数可以通过聚集的遍历方法实现迭代功能。
因为迭代的逻辑是由聚集对象本身提供的,所以这样的外禀迭代函数角色往往仅仅保持迭代的游标位置。
一个典型的由白箱聚集和外禀迭代函数组成的系统如下图, 在这个实现中具体迭代函数角色是一个外部类,
而具体聚集角色向外界提供遍历聚集元素的接口。
抽象聚集角色
这个角色规定出所有聚集必须实现的接口。迭代函数模式要求聚集对象必须有一个工厂方法,
也就是createIterator() 方法,以向外界提供迭代函数对象的实例。
public abstract class Aggregate { /** * 工厂方法:返还一个迭代函数对象 * @return */ public Iterator createIterator(){ return null; } }
抽象迭代函数角色
这个角色声明了具体迭代函数需要实现的接口,也就是提供迭代功能的各个方法,
有时也叫做迭代方法(Iteration Method).
public interface Iterator { /** * 迭代方法:移动到第一个元素 */ void first(); /** * 迭代方法:移动到下一个元素 */ void next(); /** * 迭代方法:是否是最后一个元素 * @return */ boolean isDone(); /** * 迭代方法:返还当前元素 * @return */ Object currentItem(); }
具体聚集角色
具体聚集角色实现了抽象聚集角色所要求的接口,也就是 createIterator() 方法。
此外,还有遍历方法getElement()向外界提供聚集元素,
而遍历方法size()向外界提供聚集的大小等。
public class ConcreteAggregate extends Aggregate { private Object[] objs = {"Monk Tang","Monkey","Pigsy","Sandy","Horse"}; public Iterator createIterator(){ return new ConcreteIterator(this); } /** * 取值方法:向外界提供聚集元素 * @param index * @return */ public Object getElement(int index){ if(index<objs.length){ return objs[index]; }else{ return null; } } /** * 取值方法:向外界提供聚集的大小 * @return */ public int size(){ return objs.length; } }
可以看出,这个具体聚集对象封装了一个数组,数组的元素在类被加载时就被初始化了。
为简化起见,在这个实现里聚集对象是一个不变对象。
如果一个对象的内部状态在对象创建后就不再变化,这个对象就称为不变对象。
如果一个聚集对象的内部状态可以改变的话,那么在迭代过程中,一旦聚集元素发送改变(比如一个元素被删除,或者一个新的元素被加进来),就会影响迭代过程,可能是迭代无法给出正确的结果。
迭代函数模式并不要求聚集对象是不变对象,是否将聚集设计成为不变对象仅仅是一个具体实现的问题。
具体迭代函数角色
public class ConcreteIterator implements Iterator { private ConcreteAggregate agg; private int index = 0; private int size=0; public ConcreteIterator(ConcreteAggregate agg){ this.agg = agg; size=agg.size(); index=0; } @Override public void first() { // TODO Auto-generated method stub index=0; } @Override public void next() { // TODO Auto-generated method stub if(index<size){ index++; } } @Override public boolean isDone() { // TODO Auto-generated method stub return (index>=size); } @Override public Object currentItem() { // TODO Auto-generated method stub return agg.getElement(index); } }
可以看出,具体迭代函数的构造函数接收一个具体聚集对象作为参量,这就使得外禀迭代函数可以根据这一引用控制聚集对象。
客户端角色
public class Client { private Iterator it; private Aggregate agg = new ConcreteAggregate(); public void operation(){ it = agg.createIterator(); while(!it.isDone()){ System.out.println(it.currentItem().toString()); it.next(); } } public static void main(String[] args) { Client client = new Client(); client.operation(); } }
黑箱聚集与內禀迭代模式
一个黑箱聚集不向外部提供遍历自己元素对象的接口,因此,这些元素对象只可以被聚集内部成员访问。
由于內禀迭代函数恰好是聚集内部的成员子类,因此,內禀迭代子对象是可以访问聚集的元素的。
为了说明黑箱方案的细节,这里给出一个示意性的黑箱实现,如下图。
在这个实现里,具体聚集类ConcreteAggregate含有一个内部成员类ConcreteIterator,
也就是实现了抽象迭代函数接口的具体迭代函数类,同时聚集并不向外界提供访问自己内部元素的方法。
抽象聚集角色
public abstract class Aggregate { /** * 工厂方法:返还一个迭代函数对象 * @return */ public abstract Iterator createIterator(); }
抽象迭代函数角色
public interface Iterator { /** * 迭代方法:移动到第一个元素 */ void first(); /** * 迭代方法:移动到下一个元素 */ void next(); /** * 迭代方法:是否是最后一个元素 * @return */ boolean isDone(); /** * 迭代方法:返还当前元素 * @return */ Object currentItem(); }
具体聚集角色
public class ConcreteAggregate extends Aggregate { private Object[] objs = {"Monk Tang","Monkey","Pigsy","Sandy","Horse"}; public Iterator createIterator(){ return new ConcreteIterator(); } private class ConcreteIterator implements Iterator{ private int currentIndex=0; @Override public void first() { currentIndex=0; } @Override public void next() { if(currentIndex<objs.length){ currentIndex++; } } @Override public boolean isDone() { return (currentIndex == objs.length); } @Override public Object currentItem() { return objs[currentIndex]; } } }
客户端角色
public class Client { private Iterator it; private Aggregate agg = new ConcreteAggregate(); public void operation(){ it = agg.createIterator(); while(!it.isDone()){ System.out.println(it.currentItem().toString()); it.next(); } } public static void main(String[] args) { Client client = new Client(); client.operation(); } }
白箱聚集可以是不变对象吗
一个白箱聚集对象可能是不变对象,前提是它的内部状态,包括聚集元素是不会改变的。
一个白箱聚集对象自行向外界提供遍历方法,而外禀迭代函数对象必须存储迭代的游标,
因此,在迭代过程中白箱聚集本身并不存储游标,所以它的状态是可以保持不变的。
黑箱聚集可以是不变对象吗
一个黑箱聚集对象不可能是不变对象,因为它的内部状态是会改变的。
一个黑箱聚集对象自行向外界提供一个內禀迭代子对象,而迭代子对象必须存储迭代的游标,
因此,在迭代过程中迭代子对象的状态是会改变的。这就意味着聚集对象的状态也是会改变的。
迭代函数模式的实现
迭代函数模式的实现可以很复杂。
已经涉及到迭代函数模式在实现上的两种可能:外禀迭代函数和內禀迭代函数。
主动迭代函数和被动迭代函数
迭代函数是主动的还是被动的,是相对客户端而言的。
如果客户端控制迭代的进程,那么这样的迭代函数就是主动迭代函数;
相反就是被动迭代函数。
使用主动迭代函数的客户端会明显调用迭代函数的next()等的迭代方法,
在遍历过程中向前行进;而客户端在使用被动迭代函数时,客户端并不明显的调用迭代方法,迭代函数自行推荐遍历过程。
何时使用內禀迭代函数和外禀迭代函数
本文所指的內禀迭代函数(Intrinsic Iterator)时定义在聚集结构内部的迭代函数,
而外禀迭代函数(Extrinsic Iterator)时定义在聚集结构外部的迭代函数。
Java语言的AbstractList类,选择了使用內禀迭代函数类。也即Itr。
但同时这个类也向外部提供自己的遍历方法,也就是说,如果使用AbstractList聚集,
也同样可以定义自己的外禀迭代函数。
那么,在什么情况下选择內禀迭代函数,什么情况下选择外禀迭代函数呢?
一个外禀迭代函数往往仅存储一个游标,因此如果有几个客户端同时进行迭代的话,那么可以使用几个外禀迭代函数对象,由每一个迭代函数对象控制一个独立的游标。但是外禀迭代函数要求聚集对象向外界提供遍历方法,因此会破坏聚集的封装。如果某一个客户端可以修改聚集元素的话,迭代会给出不自恰的结果,甚至影响到系统其它部分的稳定性,造成系统崩溃。
使用外禀迭代函数的一个重要理由是它可以被几个不同的方法和对象共同享用和控制。
使用內禀迭代函数的优点是它不破坏对聚集的封装。
等等其他迭代实现
迭代函数模式的优点和缺点
优点:
1. 迭代函数模式简化了聚集的界面。迭代函数具备了一个遍历接口,这样聚集的接口就不必具备遍历接口。
2. 每一个聚集对象都可以有一个或一个以上的迭代函数对象,每一个迭代函数的迭代状态可以是彼此独立的。因此一个聚集对象可以同时有几个迭代在进行之中。
3. 由于遍历算法被封装在迭代函数角色里面,因此迭代的算法可以独立于聚集角色变化。由于客户端拿到的是一个迭代函数对象,因此,不必知道聚集对象的类型,就可以读取和遍历聚集对象。这样即使聚集对象的类型发生变化,也不会影响到客户端的遍历过程。
缺点:
1. 迭代函数模式给客户daunt一个聚集被顺序化的错觉,因为大多数情况下聚集的元素并没有确定的顺序,但是迭代必须以一定的线性顺序进行。如果客户端误以为顺序是聚集本身具有的特性而过度依赖于聚集元素的顺序,很可能得出错误的结果。
2. 迭代函数给出的聚集元素没有类型特征。一般而言,迭代函数给出的元素都是Object类型,因此,客户端必须具备这些元素类型的知识才能使用这些元素。
一个例子