设计模式之迭代器模式(Iterator Pattern)

来源:互联网 发布:淘宝白色外套 编辑:程序博客网 时间:2024/06/06 01:20
    这篇博客,我们要详细讲解的是迭代器模式(Iterator Pattern),将要讲解的内容有:迭代器模式的定义,作用,详细设计分析等方面。

一、Pattern name

迭代器模式(Iterator Pattern) : 提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。——《设计模式 可复用面向对象软件的基础》

二、Problem

在我们实际编程的过程中,经常会遇到下面这种情况:
当我们需要遍历某个集合的时候,常常调用某个类的一个方法,返回一个集合类型的数据,如下:

private ArrayList<Book> bookList = new ArrayList<Book>();public ArrayList<Book> getBookList(){    return bookList;}

然后,再依据调用方法返回的ArrayList类型的bookList进行遍历操作。但是,这种方式将底层数据的实现方式和数据结构暴露出来,如果未来有一天想要改变底层数据结构,换为数组或者其他集合类进行实现,那么原来调用这个方法以实现遍历的那个地方就会报错。

那么,我们如何设计,才能消除以后可能发生的底层数据结构变化对整个程序的影响呢?如何设计,才能不把底层实现的数据结构暴露出来呢?这就要用到迭代器模式(Iterator Pattern)。

三、Solution

首先,我们来看一下,迭代器模式的UML类图结构:

这里写图片描述

上图是最基本,最简单情况的类图,从图中右下角处的Client Code我们可以看出,Client在需要遍历目标集合的时候,无需知道具体是用哪种数据结构实现的,它只需要通过接口Aggregate,获取一个实现了这个接口的类的对象,并调用createIterator()方法,就会获得目标集合的一个迭代器(Iterator),而这个迭代器提供了first(),next(),isDone(),currentItem()等方法,Client通过这些方法就可以成功完成对目标集合的遍历操作。

此时,如果我们想改变集合的底层实现的数据结构类型:
那么直接替换掉ConcreteAggregate就可以了,当然了,从图中我们可以看出ConcreteAggregate和ConcreteIterator耦合程度很严重,所以在更换ConcreteAggregate的时候,很可能也需要适当调整ConcreteIterator的实现代码,甚至是需要重新编写ConcreteIterator的代码。

如果我们想要增加一种底层实现的数据结构类型:
那么可以增加一个ConcreteAggregate2,然后同样实现Aggregate抽象接口方法,然后同样由于ConcreteAggregate和ConcreteIterator耦合度很严重,很有可能需要增加一个全新的ConcreteIterator的实现类,但是实现的遍历方式依旧和原来相同。所以很有可能会出现如下所示的变化:

这里写图片描述

你可能会想,我只是增加了一个底层数据结构类型,但是遍历策略没有发生改变,可不可以通过某种巧妙的设计,不用改变右边的Iterator实现类,就可以实现数据结构类型的增加呢?

其实,我在学习的时候,也产生了这样的疑问,经过我查阅资料以及分析讨论,我觉得答案是:不一定。因为不同的底层数据结构类型之间,有的差异很小,有的差异很大。对于差异很小的,我们只要提出一个抽象父类或接口,比如图中的List,我们把它改为一个抽象类或接口,其中定义了一些ListIterator实现需要用到的方法,然后再创建两个不同的实现List接口的实现类或List抽象类的继承子类ArrayList和LinkedList(或者是MusicList和MovieList等),这样虽然将底层数据结构类型在原来的ArrayList的基础上增加了一种新的实现方式:LinkedList,但是此时的遍历策略并不需要做出任何改变,只要新增加的数据结构类型实现了List中定义的方法,那就可以很容易做到和ListIterator完美结合。

但是我们要注意,如果新增加的数据结构类型与原有的数据结构类型相差太多,无法实现父类或接口(List)定义的方法,那么就必须增加一个Iterator的实现类,虽然实现的遍历策略是相同的,没有发生变化。

如果我们想要改变现有的遍历策略
那么可以改变原有的ConcreteIterator的实现类,来实现新的遍历方式,当然了这个实现类是在具体制定的ConcreteAggregate的基础上完成的,受到ConcreteAggregate的影响。

如果我们想要增加一种遍历策略:
那么可以增加一个ConcreteIterator的实现类,来实现新的遍历方式,当然了这个实现类是在具体制定的ConcreteAggregate的基础上完成的,受到ConcreteAggregate的影响。

四、Consequences

当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,你就应该考虑用迭代器模式。当你需要对聚集有多种方式遍历时,可以考虑用迭代器模式。

迭代器模式作用:

访问一个聚合对象的内容而无需暴露它的内部表示。
支持对聚合对象的多种遍历。
为遍历不同的聚合结构提供一个统一的接口。
它支持以不同的方式遍历一个聚合。
迭代器简化了聚合的接口。
在同一个聚合上可以有多个遍历。

五、迭代器模式的典型应用

其实,迭代器模式在很多高级语言中都有体现,甚至有完整全套的包装实现。在Java中,就有很多地方应用了迭代器模式。我们来举一个例子(对Java源码进行了适当的修改):

迭代接口:Iterator接口

public interface Iterator{    //判断是否存在下一个元素    public abstract boolean hasNext();    //返回下一个可用的元素    public abstract Object next();    //移除当前元素    public abstract void remove();}

容器接口:List接口(相当于Aggregate),定义了iterator()方法(相当于createIterator)

public interface List{    ...    //取得对所有元素的遍历。可以通过Iterator提供的方法遍历集合的元素    public abstract Iterator iterator();    ...}

容器接口List的实现类ArrayList(相当于ConcreteAggregate)

public class ArrayList implements List {    ...     //负责创建具体迭代器角色的工厂方法    public Iterator iterator() {        //把遍历委让给Iterator的实现类Itr。        return new Itr();    }    ...}

迭代接口Iterator的实现类

private class Itr implements Iterator {        ...    }

六、常见疑问解答及其他

疑问1:可能有的人会有这样的疑问,为什么不设计成这样:

在Client中:

Aggregate a = new ConcreteAggregate();Iterator i = new ConcreteIterator(a);

通过这种方式,就可以愉快地在Client中任意组合不同的集合实现类和不同的遍历策略类,如果有三种集合实现类,有两种遍历策略类,就可以实现6种巧妙组合。多完美,为什么不这样做呢?

答案是这样的:如果这样做呢,一,Client需要知道Aggregate,ConcreteAggregate,Iterator,ConcreteIterator,对这四个都会产生相应的关联关系,增加了整个系统的关系复杂度。二,也是最重要的,如果把集合类和遍历策略的匹配放到Client中,那么Client必须知道,每个遍历策略具体是什么?而且,它需要准确地知道哪个集合类可以和哪个遍历策略类组合,哪个集合类不能和哪个策略类组合。因为我们不得不承认,并不是每个遍历策略类在所有的集合类上都是可行的。而且,要Client来负责匹配分析,几个类之间的关系,特别是和Client之间的关系会非常复杂,耦合度会非常高。

疑问2:如何构建一个健壮的迭代器

所谓健壮的迭代器是指:能够保证插入和删除操作不会干扰遍历,且不需拷贝该聚合。

我们知道在遍历一个聚合的同时更改这个聚合可能是危险的,如果在遍历聚合的时候增加或删除该聚合元素,可能会导致两次访问同一个元素或者遗漏掉某个元素。一个简单的解决方法是拷贝该集合,并对该拷贝实施遍历,但一般来说这样做代价太高。所以,我们需要努力构造一个健壮的迭代器。

当然了,有许多方法来实现健壮的迭代器,其中大多数需要向这个聚合注册该迭代器。当插入或删除元素时,该聚合要么调整迭代器的内部状态,要么在内部的维护额外的信息以保证正确的遍历。

七、总结

当然了,迭代器模式的思想博大精深。我们需要学习和分析的绝不是仅仅上面讨论的这些,除此之外,还有很多很多。如果你有新的见解,不同的分析,欢迎大家一起讨论,一起进步。

2 0