从Java类库看设计模式 (Composite,Strategy,Iterator) 出处 IBM DW 刘武东 (wdliu@chinaren.com) 2002 年 1 月 本文除了还会介绍一个结构型的Composite模式之外,还会有两个行为模式登场。实际上在前面的内容中,我们已接触到行为模式了:Observer和 Command就是两个典型的行为模式。行为模式更多的注重于算法和对象建间职责的分配,也就是说,他会更多的关注于这个模式系统之类的各对象协作间的语 义,及在对象间进行通讯的流控制。 Composite模式 毫无疑问的,AWT中的Component-Container体系就是个非常好的Composite模式的例子。Container继承于 Component,而Container中有能包含有多个Component,因为Container实际上也是Component,因而 Container也能包含Container。这样通过Component-Container结构的对象组合,形成一个树状的层次结构。这也就是 Composite模式所要做的。 Composite模式是为了简化编程而提出的,一般的在编程的时候,如果严格的区分Component和Container的话,有时候会带来许多不 便,而且这些往往是没有必要的。比如,我要在一个Container中放置一个Component,我并不必知道这个Component到底是个 Container,或就是个一般的Component,在父级容器中所要做的,只是记录一个Component的引用,在需要的时候调用 Component的绘制方法来显示这个Component。当这个Component确实是个Container的时候,他能通过Container重 载后的绘制方法,完成对这个容器的显示,并把绘制消息传递给到他的子对象去。也就是说,对一个父级容器而言,他并不不关心,其子对象到底是个 Component,还是个Container。他需要将Component和Container统一对待。
图十一:Composite模式的类图 Composite模式比较简单,实现起来也不复杂,不过有一定的局限性。比如,在处理树的时候,我们往往需要处理三类对象:子树,页节点和非页节点。而 在Composite模式中对于子树和非叶节点的区分并不明显,而是把他们合成为一个Composite对象了。而且在GOF给出的Composite的 模式中,对于添加,删除子节点等属于Composite对象的的方法,是放在了Component对象中的,这虽然在实现的时候能区分开来,但容易造成一 些概念上的误解。 由上所叙,我们能提出一个改进了的Composite模式,引入子树对象,从而将子树和非叶节点分开,如下图所示: 图十二:Composite模式的一种变体 虽然将Composite从Component类层次中分离出来,但并没有损害Composite模式的内涵。这样做不一定就会比上面的那个要好,各有不同的应用,不过有时候用这样的方法来处理子树要容易些,概念上也更为清晰。 下面的代码,给出了一个Composite模式简单的Java实现: public abstract class Component{ public abstract void operation(); public void add(Component component){}; public void remove(Component component){}; } import java.util.*; public class Composite extends Component{ String name; ArrayList children = new ArrayList(); public Composite(String name){ this.name = name; } public void add(Component component){ children.add(component); } public void remove(Component component){ children.remove(component); } public void operation(){ System.out.println(name); Iterator iterator = children.iterator(); while(iterator.hasNext()){ Component child = (Component)iterator.next(); child.operation(); } } } public class Leaf extends Component{ String name; public Leaf(String name){ this.name = name; } public void operation(){ System.out.println(name); } } Strategy模式 Strategy模式主要用来将算法实现从类中分离出来,并封装在一个独立的类中。更简单的说,对象和其行为(behaviour)这本来紧密联系的两部 分被解耦,分别放在了两个不同的类中。这使得对同一个行为,能方便的在所有时候转换不同的实现算法。而通过对策略的封装,为其提供统一的接口,也能非常容 易的引入新的策略。 AWT的LayoutManager,是Strategy模式的一个例子。对于GUI而言,每个组件(Component)在容器中 (Container)的排放是需要遵循一定的算法的。通常的方法是使用绝对坐标,就像VB,Delphi之类的工具所作的那样,记录每个组件在容器中的 位置。这当然会带来一些问题,比如在窗体缩放的时候,就需要手工编码改动组件的大小和位置,以使得原来的比例得以保存。而在AWT中,引入了布局管理器 (LayoutManager)的概念,使得布局的方法大大丰富,编码过程也变得简单。 一个容器,比如Applet,Panel等,仅仅记录其包含的组件,而布局管理器中封装了对容器中组件进行布局的算法,具体地说,就是指明容器中组件的位 置和尺寸的大小。通过布局管理器,你只需要确定想放置的组件间的相对位置即可,这一方面简化编码,另一方面也有助于实现软件的平台无关性。 图十三:AWT中的容器和布局管理器的关系 每一个容器均有一个布局管理器,当容器需要布置他的组件时,他调用布局管理器的方法布置容器内的组件。LayoutManager2继承于 LayoutManager,提供更为细致的布局功能,他能让布局管理器为组件加上约束条件已确定组件怎么被布置。例如,为了确定组件被摆放在边框内的位 置,BorderLayout在他的组件上加上方向指示。 特别的,通过实现LayoutManager或LayoutManager2接口,能非常容易实现自定义的布局策略。 回到模式的话题上来,如果有几个非常相似的类,其差别仅仅是在个别行为上的动作不同,这时候就能考虑使用Strategy模式。这样,通过策略组合,将原 来的多个类精简为一个带有多个策略的类。这非常符合OO设计的原则:找到变化的部分,并将其封装起来!Strategy模式同样的为子类继承提供了一个好 的替代方案,当使用继承机制的时候,行为的改动是静态的,你指能够改动一次--而策略是动态的,能在所有时候,转换所有次数。更为重要的是,策略对象能在 不同的环境中被不同的对象所共享。以布局管理器为例,虽然每一个容器只有一个布局管理器,不过一个布局管理器能为多个容器工作。 图十四:Strategy模式的类图 Strategy模式也有一些缺点,比如,应用程式必须知道所有的策略对象,并从中选者其一。而且在策略对象被使用的时候,他和Context对象之间通 常是紧耦合的,Context对象必须为策略对象提供和具体算法相关的数据或其他的东西,而这些数据的传递可能并不能够风装载抽象地策略类中,因为并不是 所有的算法都会需要这些数据的。另外,因为策略对象通常由应用程式所创建,Context对象并不能够控制Strategy的生命期,而在概念上,这个策 略应该从属于Context对象,其生命期不应该超出Context的范围对象。 通常的,Strategy非常容易和Bridge模式相混淆。确实,他们有着非常相近的结构,不过,他们却是为解决不同的问题而设计的。Strategy模式注重于算法的封装,而Bridge模式注重于分离抽象和实现,为一个抽象体系提供不同的实现。 Iterator 模式 Iterator模式用来规格化对某一数据结构的遍历接口。 JDK中在Collection Framework中引入了Iterator接口,提供对一个Collection的遍历。每一个Collection类中都定义有从 Collection接口中继承而来的iterator()方法,来得到一个Iterator对象,我们称之为遍历器,Iterator接口非常简单: hasNext():用来判断在遍历器中是否更有下一个元素。 next():返回遍历器中的下一个元素。 remove():在被遍历的Collection类中删除最后被返回的那个对象。 我们就以最为常用的Vector为例,看看在Collection Framework中,Iterator模式是怎么被实现的。在此之前,我们需要先了解一些Vector和Collection Framework的结构。 Collection接口作为这个Framework的基础,被所有其他的集合类所继承或实现。对Collection接口,有一个基本的实现是抽象类 AbstractCollection,他实现了大部分和具体数据结构无关的操作。比如判断一个对象是否存在于这个集合类中的contains()方 法: public boolean contains(Object o) { Iterator e = iterator(); if (o==null) { while (e.hasNext()) if (e.next()==null) return true; } else { while (e.hasNext()) if (o.equals(e.next())) return true; } return false; } 而这其中调用的iterator()方法是个抽象方法,有赖于具体的数据结构的实现。不过对于这个containers()方法而言,并不必知道具体的 Iterator实现,而只需要知道他所提供的接口,能够完成某类任务既可,这就是抽象类中抽象方法的作用。其他的在 AbstractCollection中实现的非抽象方法,大部分都是依赖于抽象方法iterator()方法所提供的Iterator接口来实现的。这 种设计方法是引入抽象类的一个关键所在,值得仔细领悟。 List接口继承Collection接口,提供对列表集合类的抽象;对应的AbstractList类继承AbstractCollection,并实 现了List接口,作为List的一个抽象基类。他对其中非抽象方法的实现,也大抵上和AbstractCollection相同,这儿不再赘叙。 而对应于Collection的Iterator,List有其自己的ListIterator,ListIterator继承于Iterator,并添加了一些专用于List遍历的方法: boolean hasPrevious():判断在列表中当前元素之前是否存在有元素。 Object previous():返回列表中当前元素之前的元素。 int nextIndex(): int previousIndex(): void set(Object o): void add(Object o): ListIterator针对List,提供了更为强劲的功能接口。在AbstractList中,实现了具体的iterator()方法和listIterator()方法,我们来看看这两个方法是怎么实现的: public Iterator iterator() { return new Itr(); //Itr是个内部类 } private class Itr implements Iterator { int cursor = 0;//Iterator的计数器,指示当前调用next()方法时会被返回的元素的位置 int lastRet = -1;//指示刚刚通过next()或previous()方法被返回的元素的位置,-1 //表示刚刚调用的是remove()方法删除了一个元素。 //modCount是定义在AbstractList中的字段,指示列表被修改的次数。Iterator用//这个值来检查其包装的列表是否被其他方法所非法修改。 int expectedModCount = modCount; public boolean hasNext() { return cursor != size(); } public Object next() { try { //get方法仍然是个抽象方法,依赖于具体的子类实现 Object next = get(cursor); //检查列表是否被不正确的修改 checkForComodification(); lastRet = cursor++; return next; } catch(IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification(); try { //同样remove(int)也依赖于具体的子类实现 AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch(IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 这儿的设计技巧和上面相同,都是使用抽象方法来实现一个具体的操作。抽象方法作为最后被实现的内容,依赖于具体的子类。抽象类看起来非常像是个介于接口和子类之间的一个东西。 从设计上来讲,有人建议所有的类都应该定义成接口的形式,这当然有其道理,但多少有些极端。当你需要最大的灵活性的时候,应该使用接口,而抽象类却能够提供一些缺省的操作,最大限度的统一子类。抽象类在许多应用框架(Application Framework)中有着非常重要的作用。例如,在一个框架中,能用抽象类来实现一些缺省的服务比如消息处理等等。这些抽象类能够让你非常容易并且自然的把自己的应用嵌入到框架中去。而对于依赖于每个应用具体实现的方法,能通过定义抽象方法来引入到框架中。 其实在老版本的JDK中也有类似的概念,被称为Enumeration。Iterator其实和Enmeration功能上非常相似,只是多了删除的功 能。用Iterator不过是在名字上变得更为贴切一些。模式的另外一个非常重要的功用,就是能够形成一种交流的语言(或说文化)。有时候,你说 Enumeration大家都不明白,说Iterator就都明白了。 小结: 这部分介绍了三个模式:Composite,Strategy和Iterator。Composite是个结构性的模式,用来协调整体和局部的关系,使之 能够被统一的安排在一个树形的结构中,并简化了编程。Strategy模式和Bridge模式在结构上非常相似,不过和Bridge不同在于,他是个行为 模式,更侧重于结构的语义及算法的实现。他使得程式能够在不同的算法之间自由方便的作出选择,并能够在运行时转换到其他的算法,非常大程度上增加了程式的 灵活性。Iterator模式提供统一的接口操作来实现对一个数据结构的遍历,使得当数据结构的内部算法发生改动时,客户代码不必所有的变化,只需要改动 相应的Iterator实现,就能无缝的集成在原来的程式中。 参考资源: 设计模式:可复用面向对象软件的基础 机械工业出版社 Java2类库增补版 机械工业出版社 Java2图像设计:AWT卷 机械工业出版社 可视化面向对象建模技术 北京航天航空工业大学出版社 DK1.3原始码 UML用户指南 机械工业出版社
转自:
http://www.sudu.cn/info/article/articleInfo.php?aId=257991
