总述
本章所讨论的是把对象装进聚集(collections)里面。
当你想把多个来源不同的代码整合到一起时,你可能会想对这些源码做一些修改,但是任何一个源码的提供者都不希望你这样做。
因为你会更改已经存在的代码,这不符合前面所讲的原则。
The Iterator Pattern
它是用一个抽象的接口去适配各种不同的容器,这样一来就屏蔽了各容器自身的遍历方法,转而使用通用的遍历手段来进行遍历。
具体的方法就是某具体的容器类只要继承这个iterator接口即可。
这些具体的容器也被称为聚合。
Adding an Iterator to DinerMenu
具体做法分为如下几步:
1、定义一个Iterator接口,里面含有hasNext()和next()方法,前者判断是否还有元素,后者则是取下一元素。
2、具体的容器迭代器类要去实现这个Iterator接口。
3、在具体的容器类中创建一个方法,一般叫createIterator,来返回该容器的迭代器。
经过这些步骤以后,我们再也不用再关注具体容器的具体实现细节了,因为它们各自的迭代器能够带替它们为我们服务,而且这些迭代器能让我们更加专注于自己的本分。
但是,问题来了,一个容器对应一个迭代器,那么n个容器岂不是要对应n个容器吗?我们还要管理这n个迭代器吗?这很烦呐。
我们只想管理唯一的一个迭代器,那么这一目的如何达到呢?
Making some improvements
所谓改进就是为各种不同的Iterator建立一个公共的接口。
Iterator接口中一般还含有一个remove方法,它用来移除最后一个元素。
在你遍历时如果集合发生变动得到的结果是不确定的,这是在多线程环境下而言的。
P373类图可以清晰地表达如何进一步解决公共接口的问题。
首先,具体的功能类都去实现一个抽象的功能接口,然后具体的功能类的Iterator类也都去实现公共的Iterator接口(当然,在本书中作者采用了Java提供的现成的Iterator接口),这样它们直接套用公共的Iterator就好了。
实际上这个公共的Iterator就是Java提供的接口。
Iterator Pattern defined
它的官方定义为:它提供了一种方式,这种方式可以在不暴露内部细节的前提下,顺序地访问聚类中的对象。它把遍历的任务加到了Iterator上,这简化了聚类的接口,并且把聚类应该负担的责任转交给了聚类。
Iterator Pattern有点像factory pattern。
你可以扩展共有的Iterator接口。
迭代器模式
Single Responsibility
这个原则内容:一个类只有一个可以改变的原因。
一个类应该有且只有一种功能,因为一个类的功能越多,那么它引入问题的几率越大。
类的每个功能都是不确定因素,一个类具有很多功能就意味着它有很多不确定因素,那么它被改写的可能性越高。
严格遵守这一原则可以让模块达到高内聚低耦合。
在不使用Iterator模式的情况下,用容器编写数据类会返回容器类型对象,这会破坏数据的封装性。而使用迭代器则避免了内部全部数据的暴露。
其实,这么一来客户依然没有摆脱来自一个不同的数据容器就增加一个容器对象的窘境,只不过通过Iterator模式把各种不同容器之间的类型统一为同一个类型而已,就是说一个容器类型仍然需要一个迭代器。
这样做的好处是让客户与容器对象的内部细节之间的耦合消除。
Iterators and Collections in Java5
在Java5中有一种for/in循环,可以实现容器的遍历,它的用法有点像foreach。
我又不是搞Java的,所以不用太深入了解。
正如我上边所说的,你每增加一个容器都需要重新修改客户的代码,这一点非常不好。
我们需要一种方法来统一管理这些不同的容器对象。
The Composite Pattern
Iterator Pattern的确在一定程度上解决了因遍历不同类型容器而带来的不兼容问题,但是它对各种容器自身的变化的适应性不强。
如果一个容器突然想包括另外一个不同类型的容器,这是没法做到的,因为它们之间的类型不兼容,而Iterator Pattern没法解决这个问题。
那么现在就需要复合模式了,所以由此可见复合可以解决类型不兼容的问题。
复合模式的官方定义:复合模式允许你把一个对象复合进一个树状结构来表示一种一对多的关系,这种模式可以让客户一致地对待对象和复合进来的对象。
这种模式可以让我们只需写简单的代码就可以操纵整棵树。
这种模式可以让我们忽略对象的复合和单体对象之间的区别。
P396的图展示了这种设计模式的组织结构。
客户通过操纵Component接口来操纵整棵树,除了叶子节点外,其他的都被称为复合。叶子节点只负责处理操作,它不负责处理上下层次的关系,而复合则需要处理这两种关系。
如何将The Composite Pattern和Iterator Pattern合二为一?
在Composite Pattern中component是操作对象的接口,它不仅声明了用于操作Composite Pattern本身的方法而且还添加了Iterator遍历的必要方法。
所有的component必须实现面向客户的接口,在这里具体为MenuComponent接口,由于节点和叶子经常会有自己的方法,它们可能重写继承而来的方法。
这里说最好的办法就是你在默认的方法中抛出一个运行时异常就可以了,如果孩子节点需要的话它们就直接用,否则自己实现自己所需要的方法。
叶子节点实现了符合中的具体行为,因为它们是容器中的元素。
Implementing the Composite Menu
从P400的代码来看MenuComponent是用户直接接触的类型。
所有的Menu都是MenuComponent的子类,Menu本身也是各种Menu和MenuItem的Menu。
这里所说的print由于是在一颗树形结构上,所以某一节点还需要把它的父结点路径上的所有print都调用一遍才能打印出完整的print内容,方法就是调用各自的iterator。
这样一来waitress就能很方便地整棵树的内容,而且代码极大简化,如P402。
对于客户而言,只需要把new出来的MenuComponent的对象加到Waitress对象里面就OK了。
客户对于叶子结点和中间结点是没有概念的,是完全没有意识的。
composite pattern会处理中间结点和叶子结点,这好像有点违背一个类只能有一个功能的原则,但实际上由于对中间结点和叶子结点的处理方式相同,所以composite pattern也不算是一个类有二个以上的功能了。
如何将iterator融入composite pattern中?
首先通过在MenuComponent中添加createIterator方法,这样无论是Menu和MenuItem都可以。
遍历整个二叉树的迭代器是另外继承iterator类而来的CompositeIterator,它创建iterator,取下一个结点和判断下一个结点的方法都用到了栈。
The Null Iterator
因为一个菜单项并不是集合,所以没法用iterator遍历它,但是为了让MenuComponent处理中间结点和叶子结点的时候方式一致,书中采用了两种方式来处理iterator创建函数。
我们的目的是要告诉客户已经没有可供遍历的项了,所以要么在iterator创建函数中返回null,要么返回一个专门表示没有项的iterator。
很显然后者较为合理,因为前者是Java语法层面的没有,它可以代表很多方面的没有,从业务逻辑的角度讲你无法做到一眼看出它代表什么意义。而后者是业务逻辑层面上的没有,它代表转移的意义,不会让你瞎想。后者就被称为NullIterator。
try-catch块负责错误处理与程序逻辑无关,使用它可以避免原有代码的修改,也有利于子类同名方法覆盖。
复合模式总结
它适用于由大量对象组成的不同集合,而且这些集合是以总分的方式组织在一起的,并且用户想统一管理所有对象。
所有属性结点都应该实现统一的接口,这样用户面对的就是统一的类型。
树形结构中的结点都有指向父结点的指针,这会让删除子结点更加方便。
它大大简化了客户管理大量不同集合对象的过程。