14.3 模 式 讲 解
14.3.1 认识迭代器模式
1.迭代器模式的功能
迭代器模式的功能主要在于提供对聚合对象的迭代访问。迭代器就围绕着这个“访问”做文章,延伸出很多的功能来。比如:
■ 以不同的方式遍历聚合对象,比如向前、向后等。
■ 对同一个聚合同时进行多个遍历。
■ 以不同的遍历策略来遍历聚合,比如是否需要过滤等。
■ 多态迭代,含义是:为不同的聚合结构提供统一的迭代接口,也就是说通过一个迭代接口可以访问不同的聚合结构,这就叫做多态迭代。上面的示例就已经实现了多态迭代。事实上,标准的迭代模式实现基本上都是支持多态迭代的。
注意
但是请注意:多态迭代可能会带来类型安全的问题,可以考虑使用泛型。
2.迭代器模式的关键思想
聚合对象的类型很多,如果对聚合对象的迭代访问跟聚合对象本身融合在一起的话,会严重影响到聚合对象的可扩展性和可维护性。
因此迭代器模式的关键思想就是把对聚合对象的遍历和访问从聚合对象中分离出来,放入单独的迭代器中,这样聚合对象会变得简单一些;而且迭代器和聚合对象可以独立地变化和发展,会大大加强系统的灵活性。
3.内部迭代器和外部迭代器
所谓内部迭代器,指的是由迭代器自己来控制迭代下一个元素的步骤,客户端无法干预。因此,如果想要在迭代的过程中完成工作的话,客户端就需要把操作传递给迭代器。迭代器在迭代的时候会在每个元素上执行这个操作,类似于Java的回调机制。
所谓外部迭代器,指的是由客户端来控制迭代下一个元素的步骤,像前面的示例一样,客户端必须显式地调用next来迭代下一个元素。
总体来说外部迭代器比内部迭代器要灵活一些,因此我们常见的实现多属于外部迭代器。前面的例子也是实现的外部迭代器。
4.Java中最简单的统一访问聚合的方式
如果只是想要使用一种统一的访问方式来访问聚合对象,在Java中有更简单的方式,简单到几乎什么都不用做,利用Java5以上版本本身的特性即可。
注意
请注意,这只是从访问形式上一致了,却也暴露了聚合的内部实现,因此并不能算是标准迭代器模式的实现,但是从某种意义上说,可以算是隐含地实现了部分迭代器模式的功能。
那么怎么做呢?
为了简单,让我们回到没有添加任何迭代器模式的情况下。很简单,只要让聚合对象中的结合实现泛型即可。示例如下:
这样一来,客户端的代码就可以改成使用增强的for循环来实现了,对于数组、泛型的集合都可以采用一样的方法来实现了,从代码层面上看,就算是统一了访问聚合的方式了。修改后的客户端代码如下:
14.3.2 使用Java的迭代器
大家都知道,在java.util包里面有一个Iterator的接口,在Java中实现迭代器模式是非常简单的,而且Java的集合框架中的聚合对象基本上都是提供了迭代器的。
下面就来把前面的例子改成用Java中的迭代器实现,一起来看看有些什么改变。
■ 不再需要自己实现的Iterator接口,直接实现java.util.Iterator接口就可以了。所有使用自己实现的Iterator接口的地方都需要修改过来。
■ Java中Iterator接口跟前面自己定义的接口相比,需要实现的方法是不一样的。
■ 集合已经提供了Iterator,那么CollectionIteratorImpl类就不需要了,直接删除。好了,还是一起来看看代码吧。
(1)Pay Model类没有任何变化,就不再示例了。
(2)抽象的Aggregate类就是把创建迭代器方法返回的类型转换成Java中的Iterator了。示例代码如下:
(3)原来的ArrayIteratorImpl类,实现的接口改变了,实现的代码也需要随着改变。示例代码如下:
(4)对于PayManager类,在实现创建迭代器的方法上发生了改变,不再使用自己实现的迭代器,改成Java的集合框架实现的迭代器了。示例代码如下:
(5)对于SalaryManager类,除了创建迭代器方法返回的类型改变外,其他的都没有改变,还是用ArrayIteratorImpl来实现迭代器。
(6)接下来写个客户端来测试看看。示例代码如下:
很明显,改用Java的Iterator来实现,比自己全部重新去做,还是要简单一些的。
14.3.3 带迭代策略的迭代器
由于迭代器模式把聚合对象和访问聚合的机制实现了分离,所以可以在迭代器上实现不同的迭代策略,最为典型的就是实现过滤功能的迭代器。
延伸
在实际开发中,对于经常被访问的一些数据可以使用缓存,把这些数据存放在内存中。但是不同的业务功能需要访问的数据是不同的,还有不同的业务访问权限能访问的数据也是不同的。对于这种情况,就可以使用实现过滤功能的迭代器,让不同功能使用不同的迭代器来访问。当然,这种情况也可以结合策略模式来实现。
在实现过滤功能的迭代器中,又有两种常见的需要过滤的情况,一种是对数据进行整条过滤,比如只能查看自己部门的数据;另外一种情况是对数据进行部分过滤,比如某些人不能查看工资数据。
带迭代策略的迭代器实现的一个基本思路,就是先把聚合对象的聚合数据获取到,并存储到迭代器中,这样迭代器就可以按照不同的策略来迭代数据了。
1.带迭代策略的迭代器示例
沿用上一个例子,来修改ArrayIteratorImpl简单地示意一下,不考虑复杂的算法。大致的修改如下。
■ 原来是持有聚合对象的,现在直接把这个聚合对象的内容取出来存放到迭代器中。
也就是迭代的时候,直接在迭代器中获取具体的聚合对象的元素,这样才好控制迭代的数据。
■ 在迭代器的具体实现中加入过滤的功能。
示例代码如下:
2.谁定义遍历算法的问题
在实现迭代器模式的时候,一个常见的问题就是:谁来定义遍历算法?其实带策略的迭代器讲述的也是这个问题。
在迭代器模式的实现中,常见的有两个地方可以来定义遍历算法,一个是聚合对象本身,另外一个就是迭代器负责遍历算法。
在聚合对象本身定义遍历算法的这种情况下,通常会在遍历过程中,用迭代器来存储当前迭代的状态,这种迭代器被称为游标,因为它仅用来指示当前的位置。比如在14.2.4节中示例的迭代器就属于这种情况。
在迭代器中定义遍历算法,会比在相同的聚合上使用不同的迭代算法容易,同时也易于在不同的聚合上重用相同的算法。比如上面带策略的迭代器的示例,迭代器把需要迭代的数据从聚合对象中取出并存放到自己的对象中,然后再迭代自己的数据,这样一来,除了刚开始创建迭代器的时候需要访问聚合对象外,真正迭代过程已经跟聚合对象无关了。
但是,在迭代器中定义遍历算法,如果实现遍历算法需要访问聚合对象的私有变量,那么将遍历算法放入迭代器中会破坏聚合对象的封装性。
14.3.4 双向迭代器
所谓双向迭代器的意思就是:可以同时向前和向后遍历数据的迭代器。
在Java util包中的ListIterator接口就是一个双向迭代器的示例。当然自己实现双向迭代器也非常容易,只要在自己的Iterator接口中添加向前的判断和向前获取值的方法,然后在实现中实现即可。
延续14.2.4节的示例,来自己实现双向迭代器,相同的部分就不再示范了,只演示不同的地方。
先看看新的迭代器接口。示例代码如下:
有了新的迭代器接口,也应该有新的实现。示例代码如下:
基本实现完了,写个客户端来享受一下双向迭代的乐趣。由于这个实现要考虑同时控制向前和向后迭代取值,而控制当前索引的是同一个值,因此在获取向前取值的时候,要先把已访问索引减去1,然后再取值,这个跟向后取值是反过来的,注意一下。示例代码如下:
上面的示例故意先向后取值,然后再向前取值,这样反复才能看出双向迭代器的效果。运行结果如下:
可能有些人会疑惑:为什么next1和previous1取出来的值是一样的呢?
这是因为现在是顺序迭代,当next显示第一条的时候,内部索引已经指向第二条了,所以这个时候再previous向前一条的时候,数据就是第一条数据了。
再仔细查看上面的结果,发现这个时候继续next数据时,数据还是第一条数据,同理,刚才previous向前一条的时候,内部索引已经指向第一条之前了。
14.3.5 迭代器模式的优点
■ 更好的封装性
■ 迭代器模式可以让你访问一个聚合对象的内容,而无须暴露该聚合对象的内部表示,从而提高聚合对象的封装性。
■ 可以以不同的遍历方式来遍历一个聚合
■ 使用迭代器模式,使得聚合对象的内容和具体的迭代算法分离开。这样就可以通过使用不同的迭代器的实例、不同的遍历方式来遍历一个聚合对象了,比如上面示例的带迭代策略的迭代器。
■ 迭代器简化了聚合的接口
■ 有了迭代器的接口,则聚合本身就不需要再定义这些接口了,从而简化了聚合的接口定义。
■ 简化客户端调用
■ 迭代器为遍历不同的聚合对象提供了一个统一的接口,使得客户端遍历聚合对象的内容变得更简单。
■ 同一个聚合上可以有多个遍历
■ 每个迭代器保持它自己的遍历状态,比如前面实现中的迭代索引位置,因此可以对同一个聚合对象同时进行多个遍历。
14.3.6 思考迭代器模式
1.迭代器模式的本质
迭代器模式的本质:控制访问聚合对象中的元素。
迭代器能实现“无须暴露聚合对象的内部实现,就能够访问到聚合对象中的各个元素”的功能,看起来其本质应该是“透明访问聚合对象中的元素”。
提示
但仔细思考一下,除了透明外.迭代器就没有别的功能了吗?很明显还有其他的功能,前面也讲到了一些,比如“带迭代策略的迭代器”。那么综合来看,迭代器模式的本质应该是“控制访问聚合对象中的元素”,而非单纯的“透明”。事实上,“透明”访问也是“控制访问”的一种情况。
认识这个本质,对于识别和变形使用迭代器模式很有帮助。大家想想,现在的迭代模式默认的都是向前或者向后获取一个值,也就是说都是单步迭代,那么,如果想要控制一次迭代多条怎么办呢?
这个在实际开发中是很有用的,比如在实际开发中常用的翻页功能的实现。翻页功能有如下儿种实现方式。
(1)纯数据库实现。
依靠SQL提供的功能实现翻页,用户每次请求翻页的数据,就会到数据库中获取相应的数据。
(2)纯内存实现。
就是一次性从数据库中把需要的所有数据都取出来放到内存中,然后用户请求翻页时,从内存中获取相应的数据。
上面两种方案各有优缺点:
第一种方案明显是时间换空间的策略,每次获取翻页的数据都要访问数据库,运行速度相对比较慢,而且很耗数据库资源,但是节省了内存空间;
第二种方案是典型的空间换时间,每次是直接从内存中获取翻页的数据,运行速度快,但是很耗内存。
在实际开发中,小型系统一般采用第一种方案,基本没有单独采用第二种方案的,因为内存实在是太宝贵了;中大型的系统一般是把两个方案结合起来,综合利用它们的优点,而又规避它们的缺点,从而更好地实现翻页的功能。
(3)纯数据库实现+纯内存实现。
思路是这样的:如果每页显示10条记录,根据判断,用户很少翻到10页以后,那好,第一次访问的时候,就一次性从数据库中获取前10页的数据,也就是100条记录,把这100条记录放在内存里面。
这样一来,当用户在前10页内进行翻页操作的时候,就不用再访问数据库了,而是直接从内存中获取数据,速度就快了。
当用户想要获取第11页的数据,这个时候才会再次访问数据库。对于这个时候到底获取多少页的数据,简单的处理就是继续获取10页的数据。比较好的方式就是根据访问统计进行衰减访问,比如折半获取,也就是第一次访问数据库获取10页的数据,那么第二次就只获取5页,如此操作直到一次从数据库中获取一页的数据。这也符合正常规律,因为越到后面,被用户翻页到的机会也就越小了。
对于翻页的迭代,后面将给大家一个简单的示例。
2.何时选用迭代器模式
建议在以下情况中选用迭代器模式。
■ 如果你希望提供访问一个聚合对象的内容,但是又不想暴露它的内部表示的时候,可以使用迭代器模式来提供迭代器接口,从而让客户端只是通过迭代器的接口来访问聚合对象,而无须关心聚合对象的内部实现。
■ 如果你希望有多种遍历方式可以访问聚合对象,可以使用迭代器模式。
■ 如果你希望为遍历不同的聚合对象提供一个统一的接口,可以使用迭代器模式。
14.3.7 翻页迭代
在上面讲到的翻页实现机制中,只要使用到内存来缓存数据,就涉及到翻页迭代的实现。简单点说,就是一次迭代,会要求迭代取出一页的数据,而不是一条数据。
其实实现翻页迭代也很简单,主要是把原来一次迭代一条数据的接口,都修改成一次迭代一页的数据就可以了。在具体的实现上,又分成顺序翻页迭代器和随机翻页迭代器。
1.顺序翻页迭代器示例
(1)先看看迭代器接口的定义。示例代码如下:
(2)PayModel和前面的示例是一样的,这里就不再赘述。
(3)接下来看看SalaryManager的实现,有如下改变。
■ 不用再实现获取聚合对象大小和根据索引获取聚合对象中的元素的功能了。
■ 在准备测试数据的时候,多准备几条,方便看出翻页的效果。
示例代码如下:
(4)来看看如何实现迭代器接口。示例代码如下:
(5)写个客户端测试一下。示例代码如下:
运行结果如下:
仍然是顺序迭代的,也就是获取完第二页数据,内部索引就指向后面了,这个时候再运行向前一页,取的就还是第二页的数据了。
2.随机翻页迭代器示例
估计看到这里,有些朋友会想,实际应用中,用户怎么会这么老实,按照顺序访问?通常情况都是随意的访问页数,比如看了第一页可能就直接看第三页了,看完第三页他又想看第一页。
这就需要随机翻页迭代器了,也就是可以指定页面号和每页显示的数据来访问数据的迭代器。下面来看看示例。
(1)修改迭代接口的方法。不需要再有向前和向后的方法,取而代之的是指定页面号和每页显示的数据来访问的方法。示例代码如下:
(2)定义了接口,看看具体的实现。示例代码如下:
(3)写个客户端,测试看看,是否能实现随机的翻页。示例代码如下:
测试结果如下:
14.3.8 相关模式
■ 迭代器模式和组合模式
这两个模式可以组合使用。
组合模式是一种递归的对象结构,在枚举某个组合对象的子对象的时候,通常会使用迭代器模式。
■ 迭代器模式和工厂方法模式
这两个模式可以组合使用。
在聚合对象创建迭代器的时候,通常会采用工厂方法模式来实例化相应的迭代器对象。