当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。
比如,一个人的出生年月日是“不变”属性,而一个人的年龄便是“只读”属性,但是不是“不变”属性。
随着时间的变化,一个人的年龄会随之发生变化,而人的出生年月则不会变化。
这就是“不变”和“只读”的区别。
4.2策略(Strategy)模式
策略模式属于对象的行为模式[[GOF95]。
其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。
策略模式使得算法可以在不影响到客户端的情况下发生变化。
4.2.1策略模式的结构
策略又称做政策(Policy)模式[[GOF95]。
下面就以一个示意性的实现讲解策略模式实例的结构,如下图所示。
这个模式涉及到三个角色:
Ø环境(Context)角色:
持有一个Strategy类的引用。
Ø抽象策略(Strategy)角色:
这是一个抽象角色,通常由一个接口或抽象类实现。
此涌色给出所有的具体策略类所需的接口。
Ø具体策略(ConcreteStrategy)角色:
包装了相关的算法或行为。
首先看一看环境角色的源代码,如代码清单1所示,这个类持有一个对策略角色的引
代码清单1:
环境类的源代码
publicclassContext
{
privateStrategystrategy;
/**
*策略方法
*/
publicvoidcontextInterface()
{
strategy.strategyInterface();
}
抽象策略类的源代码如代码清单2所示,这个抽象类可以用Java接口取代。
抽象策略类规定所有具体策略类必须实现的接口。
代码清单2:
抽象策略类的源代码
abstractpublicclassStrategy
{
/**
*策略方法
*/
publicabstractvoidstrategyInterface();
}
具体策略类实现了抽象策略类所声明的接口,如代码清单3所示。
代码清单3:
具体策略类的源代码
publicclassConcreteStrategyextendsStrategy
{
/**
*策略方法
*/
publicvoidstrategyInterface()
{
//writeyoualgorithmcodehere
}
}
这里所给出的仅仅是策略模式的最小实现,因此具体策略角色才只有一个。
一般而言,有意义的策略模式的应用都会涉及到多于一个的具体策略角色。
读者可以把这里给出的源代码当做策略模式的骨架,在将它使用到自己的系统中时,还需要添加上与系统有关的逻辑。
4.2.2模式的实现
策略模式的实现有以下这些需要注意的地方。
(1)经常见到的是,所有的具体策略类都有一些公有的行为。
这时候,就应当把这些公有的行为放到共同的抽象策略角色Strategy类里面。
当然这时候抽象策略角色必须要
用Java抽象类实现,而不能使用Java接口。
这其实也是典型的将代码向继承等级结构的上方集中的标准做法。
代码集中的方向如下图所示,并请阅读本书的“抽象类”一章。
(2)策略模式在每一个时刻都只能使用一个策略对象,但是有的时候一个应用程序同时与几个策略对象相联系。
换言之,在应用程序启动时,所有的策略对象就已经被创立出来,而应用程序可以在几个策略对象之间调换。
这只有在策略对象不会耗费很多计算机内存资源的情况下才可行,只有在策略对象的初始化会花费很长时间的情况下才需要。
4.3模板方法(TemplateMethod)模式
模版方法模式是类的行为模式[GOF95],准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。
不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。
这就是模版方法模式的用意。
4.3.1模版方法模式的结构
模版方法模式的静态结构如下图所示。
这里涉及到两个角色:
抽象模版(AbstractTemplate)角色有如下的责任:
Ø定义了一个或多个抽象操作,以便让子类实现。
这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
Ø定义并实现了一个模版方法。
这个模版方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。
顶级逻辑也有可能调用一些具体方法。
具体模版(ConcreteTemplate)角色有如下的责任:
Ø实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
Ø每一个抽象模版角色都可以有任意多个具体模版角色与之对应,而每一个具体模版角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
本节在下面给出示意性源代码。
首先抽象模版角色提供了一个具体方法TemplateMethodQ,此具体方法调用一些抽象方法,包括doOperation1(),doOperation2()等,如代码清单1所示。
代码清单1:
抽象模版举的示章性源代码
abstractpublicclassAbstractClass
{
/**
*模版方法的声明和实现
*/
publicvoidTemplateMethod()
{
//调用基本方法(由子类实现)
doOperation1();
//调用基本方法(由子类实现)
doOperation2();
//调用基本方法(已经实现)
doOperation3();
}
/**
*基本方法的声明(由子类实现)
*/
protectedabstractvoiddoOperationl();
/*
*基本方法的声明(由子类实现)
*/
protectedabstractvoiddoOperation2();
/**
*基本方法(已经实现)
*/
privatefinalvoiddoOperation3()
{
//dosomething
}
}
显然doOperation1(),doOperation2()等基本方法是顶级逻辑的组成步骤,这个顶级逻辑由TemplateMethodQ方法代表。
显然,抽象模版类自己并不给出这些基本方法的实现,而是把这些基本方法交给子类去实现。
具体模版角色负责实现抽象方法,如代码清单2所示。
代码清单2:
具体模版类的示意性源代码
publicclassConcreteClassextendsAbstractClass
{
/**
*基本方法的实现
*/
publicvoiddoOperation1()
{
Systern.out.prindn("doOperation1();");
}
/**
*基本方法的实现
*/
publicvoiddoOperation2()
{
//像下面这样的调用不应当发生
//doOperation3();
System.out.println("doOperation2();");
}
}
这个具体类实现了父类所声明的基本方法--doOperation1()和doOperation2(),而这两个基本方法所代表的就是强制子类实现的剩余逻辑。
4.4观察者(Observer)模式
观察者模式是对象的行为模式[GOF95],又叫做发布一订阅(Publish/Subscribe)模式、模型一视图(Model/View)模式、源一监听器(Source/Listener)模式或从属者(Dependents)模式。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某,一个主题对象。
这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
4.4.1观察者模式的结构
下面就以一个简单的示意性实现为例,讨论观察者模式的结构。
这个实现的类图如下图所示。
观察者模式的静态结构可以从类图中看清楚。
可以看出,在这个观察者模式的实现里有下面这些角色:
Ø抽象主题(Su衍ect)角色:
主题角色把所有对观察者对象的引用保存在一个聚集(比如Vector对象)里,每个主题都可以有任何数量的观察者。
抽象主题提供一个接口,可以增加和删除观察者对象,主题角色又叫做抽象被观察者(Observable)角色,般用一个抽象类或者一个接口实现。
抽象主题角色如右图所示。
Ø抽象观察者(Observer)角色:
为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。
这个接口叫做更新接口。
抽象观察者角色一般用一个抽象类或者一个接口实现,如右图所示。
在这个示意性的实现中,更新接口只包含一个方法(即update()方法),这个方法叫做更新方法。
Ø具体主题(ConcreteSu衍ect)角色:
将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。
具体主题角色又叫作具体被观察者角色(ConcreteObservable)。
具体主题角色通常用一个具体子类实现,如下图所示。
在这个示意性实现里,具体主题角色负责实现对观察者引用的聚集的管理方法。
Ø具体观察者(ConcreteObserver)角色:
存储与主题的状态自恰的状态。
具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。
如果需要,具体观察者角色可以保存一个指向具体主题对象的引用。
具体观察者角色通常用一个具体子类实现,如右图所示。
在这个示意性实现中,更新接口只有·个更新方法(也就是update()方法)。
在类图中,从具体主题角色指向抽象观察者角色的合成关系,代表具体主题对象可以含有任意多个对抽象观察者对象的引用。
当然,一个Java抽象类是不可能有实例的,因此这些引用的真实类型必然是ConcreteObserver类型;而这些引用的静态类型是Observer类型。
这意味着主题对象不需要知道引用了哪些ConcreteObserver类型,而只知道抽象Observer类型。
这就使得具体主题对象可以动态地维护一系列的对观察者对象的引用,并在需要的时候调用每一个观察者共有的update()方法。
这种做法叫做“针对抽象编程”,对这原则感兴趣的读者可以进一步阅读本书的“依赖倒转原则”一章。
下面给出·个示意性实现的Java代码。
首先,Su句ect接口实现抽象主题角色,如代码清单1所示。
代码清单1:
Subiect接口的源代码
publicinterfaceSubject
{
/**
*调用这个方法登记一个新的观察者对象
*/
publicvoidattach(Observerobserver);
/**
*调用这个方法删除一个己经登记过的观察者对象
*/
publicvoiddetach(Observerobserver);
/**
*调用这个方法通知所有登记过的观察者对象
*/
voidnotifyObservers();
}
这个抽象主题接口规定出三个子类必须实现的操作,即attach()用来增加个观察者对象:
detach()用来删除一个己经登记过的观察者对象;而notifyObserversQ用来通知各个观察者更新它们自己。
读者可以看出,attach()方法和detach()方法实际上是聚集的管理方法,抽象主题角色声明了这两个方法,实际上是要求具体子类维护一个以所有观察者对象为元素的聚集。
具体主题则是实现了抽象主题Subject接口的一个具体类,它给出了以上的三个操作的具体实现,如代码清单2所示。
从下面的源代码可以看出,这里给出的Java实现使用了一个Java向量来保存所有的观察者对象,而attach()和detach()操作则是对此向量的元素增减操作。
这些局级管理方法被移到抽象主题角色中。
代码清单2:
ConcreteSubject类的源代码
importjava.util.Vector;
importjava.util.Enumeration;
publicclassConcreteSubjectimplementsSubject
{
privateVectorobserversVector=
newjava.util.VectorQ;
/**
*调用这个方法登记一个新的观察者对象
*/
publicvoidattach(Observerobserver)
{
observersVector.addElement(observer);
l**
*调用这个方法删除一个已经登记过的观察者对象
*/
publicvoiddetach(Observerobserver)
{
observersVector.removeElement{observer);
}
/**
*调用这个方法通知所有登记过的观察者对象
*/
publicvoidnotifyObservers()
{
Enumerationenumeration=observers();
while(enumeration.hasMoreElements())
{
((Observer)
enumeration.nextElement()).update();
}
}
*这个方法给出观察者聚集的Enumeration对象
*/
publicEnumerationobservers()
{
return((Vector)
observersVector.clone()),elements();
}
}
抽象观察者角色的实现实际上是最为简单的。
它是只声明了一个方法(即update()方法)的Java接口。
这个方法被子类实现后,一被调用便更新观察者对象,如代码清单3所示。
代码清单3:
Observer接口的源代码
publicinterfaceObserver
{
/**
*调用这个方法会更新自己
*/
voidupdate();
}
具体观察者角色的实现其实只涉及update()方法的实现。
这个方法怎么实现是与应用密切相关的,因此本书只给出一个框架,如代码清单4所示。
读者可以在后面更加具体的例子中看到实现这个方法的细节。
代码清单4:
ConcreteObserver类的源代码
publicclassConcreteObserverimplementsObserver
{
/**
*调用这个方法会更新自己
*/
publicvoidupdate()
{
System.out.println("Iamnotified");
}
}
下图所示是具体主题角色调用具体观察者角色的更新方法时的时序图。
4.5迭代子(Iterator)模式
迭代子(Iterator)模式又叫游标(Cursor)模式[[GOF95],是对象的行为模式。
迭代子模式可以顺序地访问一个聚集中的元素而不必暴露聚集的内部表象。
4.5.1迭代子模式的结构
迭代子模式如何实现影响它的结构的细节,因此,本章首先给出此模式的一般性结构,然后讨论模式的实现与相应的结构问题。
一般性结构
迭代子模式的一般性结构图如下图所示。
迭代子模式涉及到以下几个角色:
Ø抽象迭代子(Iterator)角色:
此抽象角色定义出遍历元素所需的接口。
Ø具体迭代子(ConcreteIterator)角色:
此角色实现了Iterato;接口,并保持迭代过程中的游标位置。
Ø聚集(Aggregate)角色:
此抽象角色给出创建迭代子(Iterator)对象的接口。
具体聚集(ConcreteAggregate)角色:
实现了创建迭代子(Iterator)对象的接口,返回一个合适的具体迭代子实例。
Ø客户端(Client)角色:
持有对聚集及其迭代子对象的引用,调用迭代子对象的迭代接口,也有可能通过迭代子操作聚集元素的增加和删除。
4.5.2迭代子模式的实现
迭代子模式的实现可以很复杂。
在本章前面的“迭代子模式的结构”一节里,已经涉及到迭代子模式在实现上的两种可能:
外禀迭代子和内禀迭代子。
主动迭代子和被动迭代子
迭代子是主动的(Active)还是被动的(Passive),是相对于客户端而言的。
如果客户端控制迭代的进程,那么这样的迭代子就是主动迭代子;相反就是被动迭代子。
使用主动迭代子的客户端会明显调用迭代子的next()等迭代方法,在遍历过程中向前行进;而客户端在使用被动迭代子时,客户端并不明显地调用迭代方法,迭代子自行推进遍历过程。
何时使用内禀迭代子和外禀迭代子
外部迭代子(ExternalIterator)和内部迭代子(InternalIterator)是主动和被动迭代子的等义词。
这与本书所说的外禀迭代子(ExtrinsicIterator)和内禀迭代子(IntrinsicIterator)是不同的概念。
本书所指的内禀迭代子是定义在聚集结构内部的迭代子,而外禀迭代子是定义在聚集结构外部的迭代子,其他很多文献(如[[ALPERT98]中都有相同的提法。
Java语言的设计师们在设计AbstractList类时,选择了使用内禀迭代子类,也即Itr。
但同时这个类也向外部提供自己的遍历方法,换言之,如果设计师使用AbstractList聚集,也同样可以定义自己的外察迭代子。
那么在什么情况下选择内禀迭代子,什么情况下选择外禀迭代子呢?
一个外察迭代子往往仅存储一个游标,因此如果有几个客户端同时进行迭代的话,那么可以使用几个外禀迭代子对象,由每一个迭代子对象控制一个独立的游标。
但是,外禀迭代子要求聚集对象向外界提供遍历方法,因此会破坏对聚集的封装。
如果某一个客户端可以修改聚集元素的话,迭代会给出不自恰的结果,甚至影响到系统其他部分的稳定性,造成系统崩溃。
使用外禀迭代子的一个重要理由是它可以被几个不同的方法和对象共同享用和控制。
使用内禀迭代子的优点是它不破坏对聚集的封装。
静态迭代子和动态迭代子
静态迭代子由聚集对象创建,并持有聚集对象的一个快照(snapshot),在产生后这个快照的内容就不再变化。
客户端可以继续修改原聚集的内容,但是迭代子对象不会反映出聚集的新变化。
静态迭代子的好处是它的安全性和简易性,换言之,静态迭代子易于实现,不容易出现错误。
但是由于静态迭代子将原聚集复制了一份,因此它的短处是对时间和内存资源的消耗。
对大型的聚集对象来说,使用静态迭代子不是一个合适的选择。
动态迭代子则与静态迭代子完全相反,在迭代子被产生之后,迭代子保持着对聚集元素的引用,因此,任何对原聚集内容的修改都会在迭代子对象上反映出来。
完整的动态迭代子不容易实现,但是简化的动态迭代子并不难实现。
大多数Java设计师遇到的迭代子都是这种简化的动态迭代子。
为了说明什么是简化的动态迭代子,首先需要介绍一个新的概念:
FailFast。
FailFast
如果当一个算法开始之后,它的运算环境发生变化,使得算法无法进行必需的调整时,这个算法就应当立即发出故障信号。
这就是FailFast的含义。
如果聚集对象的元素在一个动态迭代子的迭代过程中发生变化时,迭代过程会受到影响而变得不能自恰。
这时候,迭代子就应当立即抛出一个异常。
这种迭代