“一对二”,“过”,“过”……这声音熟悉吗?你会想到什么?对!纸牌。在类似“斗地主”这样的纸牌游戏中,某人出牌给他的下家,下家看看手中的牌,如果要不起上家的牌则将出牌请求再转发给他的下家,其下家再进行判断。一个循环下来,如果其他人都要不起该牌,则最初的出牌者可以打出新的牌。在这个过程中,牌作为一个请求沿着一条链在传递,每一位纸牌的玩家都可以处理该请求。在设计模式中,我们也有一种专门用于处理这种请求链式传递的模式,它就是本章将要介绍的职责链模式。
16.1 采购单的分级审批
Sunny软件公司承接了某企业SCM(SupplyChain Management,供应链管理)系统的开发任务,其中包含一个采购审批子系统。该企业的采购审批是分级进行的,即根据采购金额的不同由不同层次的主管人员来审批,主任可以审批5万元以下(不包括5万元)的采购单,副董事长可以审批5万元至10万元(不包括10万元)的采购单,董事长可以审批10万元至50万元(不包括50万元)的采购单,50万元及以上的采购单就需要开董事会讨论决定。如图16-1所示:
图16-1 采购单分级审批示意图
如何在软件中实现采购单的分级审批?Sunny软件公司开发人员提出了一个初始解决方案,在系统中提供一个采购单处理类PurchaseRequestHandler用于统一处理采购单,其框架代码如下所示:
//采购单处理类
class PurchaseRequestHandler {
//递交采购单给主任
public void sendRequestToDirector(PurchaseRequest request) {
if (request.getAmount() < 50000) {
//主任可审批该采购单
this.handleByDirector(request);
}
else if (request.getAmount() < 100000) {
//副董事长可审批该采购单
this.handleByVicePresident(request);
}
else if (request.getAmount() < 500000) {
//董事长可审批该采购单
this.handleByPresident(request);
}
else {
//董事会可审批该采购单
this.handleByCongress(request);
}
}
//主任审批采购单
public void handleByDirector(PurchaseRequest request) {
//代码省略
}
//副董事长审批采购单
public void handleByVicePresident(PurchaseRequest request) {
//代码省略
}
//董事长审批采购单
public void handleByPresident(PurchaseRequest request) {
//代码省略
}
//董事会审批采购单
public void handleByCongress(PurchaseRequest request) {
//代码省略
}
}
问题貌似很简单,但仔细分析,发现上述方案存在如下几个问题:
(1)PurchaseRequestHandler类较为庞大,各个级别的审批方法都集中在一个类中,违反了“单一职责原则”,测试和维护难度大。
(2)如果需要增加一个新的审批级别或调整任何一级的审批金额和审批细节(例如将董事长的审批额度改为60万元)时都必须修改源代码并进行严格测试,此外,如果需要移除某一级别(例如金额为10万元及以上的采购单直接由董事长审批,不再设副董事长一职)时也必须对源代码进行修改,违反了“开闭原则”。
(3)审批流程的设置缺乏灵活性,现在的审批流程是“主任-->副董事长-->董事长-->董事会”,如果需要改为“主任-->董事长-->董事会”,在此方案中只能通过修改源代码来实现,客户端无法定制审批流程。
如何针对上述问题对系统进行改进?Sunny公司开发人员迫切需要一种新的设计方案,还好有职责链模式,通过使用职责链模式我们可以最大程度地解决这些问题,下面让我们正式进入职责链模式的学习。
16.2 职责链模式概述
很多情况下,在一个软件系统中可以处理某个请求的对象不止一个,例如SCM系统中的采购单审批,主任、副董事长、董事长和董事会都可以处理采购单,他们可以构成一条处理采购单的链式结构,采购单沿着这条链进行传递,这条链就称为职责链。职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理,客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上即可,实现请求发送者和请求处理者解耦。
职责链模式定义如下: 职责链模式(Chain of ResponsibilityPattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。
职责链模式结构的核心在于引入了一个抽象处理者。职责链模式结构如图16-2所示:
在职责链模式结构图中包含如下几个角色:
● Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。
● ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。
在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
职责链模式的核心在于抽象处理者类的设计,抽象处理者的典型代码如下所示:
abstract class Handler {
//维持对下家的引用
protectedHandler successor;
public void setSuccessor(Handler successor) {
this.successor=successor;
}
public abstract void handleRequest(String request);
}
上述代码中,抽象处理者类定义了对下家的引用对象,以便将请求转发给下家,该对象的访问符可设为protected,在其子类中可以使用。在抽象处理者类中声明了抽象的请求处理方法,具体实现交由子类完成。
具体处理者是抽象处理者的子类,它具有两大作用:第一是处理请求,不同的具体处理者以不同的形式实现抽象请求处理方法handleRequest();第二是转发请求,如果该请求超出了当前处理者类的权限,可以将该请求转发给下家。具体处理者类的典型代码如下:
class ConcreteHandler extends Handler{
public void handleRequest(String request) {
if (请求满足条件) {
//处理请求
}
else {
this.successor.handleRequest(request); //转发请求
}
}
}
在具体处理类中通过对请求进行判断可以做出相应的处理。
需要注意的是,职责链模式并不创建职责链,职责链的创建工作必须由系统的其他部分来完成,一般是在使用该职责链的客户端中创建职责链。职责链模式降低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。
思考
如何在客户端创建一条职责链?
16.3 完整解决方案 为了让采购单的审批流程更加灵活,并实现采购单的链式传递和处理,Sunny公司开发人员使用职责链模式来实现采购单的分级审批,其基本结构如图16-3所示:
在图16-3中,抽象类Approver充当抽象处理者(抽象传递者),Director、VicePresident、President和Congress充当具体处理者(具体传递者),PurchaseRequest充当请求类。完整代码如下所示:
//采购单:请求类
class PurchaseRequest {
private double amount; //采购金额
private int number; //采购单编号
private String purpose; //采购目的
public PurchaseRequest(double amount, int number, String purpose) {
this.amount = amount;
this.number = number;
this.purpose = purpose;
}
public void setAmount(double amount) {
this.amount = amount;
}
public double getAmount() {
return this.amount;
}
public void setNumber(int number) {
this.number = number;
}
public int getNumber() {
return this.number;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public String getPurpose() {
return this.purpose;
}
}
//审批者类:抽象处理者
abstract class Approver {
protected Approver successor; //定义后继对象
protected String name; //审批者姓名
public Approver(String name) {
this.name = name;
}
//设置后继者
public void setSuccessor(Approver successor) {
this.successor = successor;
}
//抽象请求处理方法
public abstract void processRequest(PurchaseRequest request);
}
//主任类:具体处理者
class Director extends Approver {
public Director(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 50000) {
System.out.println("主任" + this.name +"审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:"+ request.getPurpose() + "。"); //处理请求
}
else {
this.successor.processRequest(request); //转发请求
}
}
}
//副董事长类:具体处理者
class VicePresident extends Approver {
public VicePresident(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 100000) {
System.out.println("副董事长" + this.name+ "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:"+ request.getPurpose() + "。"); //处理请求
}
else {
this.successor.processRequest(request); //转发请求
}
}
}
//董事长类:具体处理者
class President extends Approver {
public President(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 500000) {
System.out.println("董事长" + this.name +"审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:"+ request.getPurpose() + "。"); //处理请求
}
else {
this.successor.processRequest(request); //转发请求
}
}
}
//董事会类:具体处理者
class Congress extends Approver {
public Congress(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
System.out.println("召开董事会审批采购单:" +request.getNumber() + ",金额:" +request.getAmount() + "元,采购目的:" +request.getPurpose() + "。"); //处理请求
}
}
编写如下客户端测试代码:
class Client {
public static void main(String[] args) {
Approver wjzhang,gyang,jguo,meeting;
wjzhang = new Director("张无忌");
gyang = new VicePresident("杨过");
jguo = new President("郭靖");
meeting = new Congress("董事会");
//创建职责链
wjzhang.setSuccessor(gyang);
gyang.setSuccessor(jguo);
jguo.setSuccessor(meeting);
//创建采购单
PurchaseRequest pr1 = new PurchaseRequest(45000,10001,"购买倚天剑");
wjzhang.processRequest(pr1);
PurchaseRequest pr2 = new PurchaseRequest(60000,10002,"购买《葵花宝典》");
wjzhang.processRequest(pr2);
PurchaseRequest pr3 = new PurchaseRequest(160000,10003,"购买《金刚经》");
wjzhang.processRequest(pr3);
PurchaseRequest pr4 = new PurchaseRequest(800000,10004,"购买桃花岛");
wjzhang.processRequest(pr4);
}
}
编译并运行程序,输出结果如下:
主任张无忌审批采购单:10001,金额:45000.0元,采购目的:购买倚天剑。
副董事长杨过审批采购单:10002,金额:60000.0元,采购目的:购买《葵花宝典》。
董事长郭靖审批采购单:10003,金额:160000.0元,采购目的:购买《金刚经》。
召开董事会审批采购单:10004,金额:800000.0元,采购目的:购买桃花岛。
如果需要在系统增加一个新的具体处理者,如增加一个经理(Manager)角色可以审批5万元至8万元(不包括8万元)的采购单,需要编写一个新的具体处理者类Manager,作为抽象处理者类Approver的子类,实现在Approver类中定义的抽象处理方法,如果采购金额大于等于8万元,则将请求转发给下家,代码如下所示:
//经理类:具体处理者
class Manager extends Approver {
public Manager(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 80000) {
System.out.println("经理" + this.name +"审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:"+ request.getPurpose() + "。"); //处理请求
}
else {
this.successor.processRequest(request); //转发请求
}
}
}
由于链的创建过程由客户端负责,因此增加新的具体处理者类对原有类库无任何影响,无须修改已有类的源代码,符合“开闭原则”。
在客户端代码中,如果要将新的具体请求处理者应用在系统中,需要创建新的具体处理者对象,然后将该对象加入职责链中。如在客户端测试代码中增加如下代码:
Approver rhuang;
rhuang = new Manager("黄蓉");
将建链代码改为:
//创建职责链
wjzhang.setSuccessor(rhuang); //将“黄蓉”作为“张无忌”的下家
rhuang.setSuccessor(gyang); //将“杨过”作为“黄蓉”的下家
gyang.setSuccessor(jguo);
jguo.setSuccessor(meeting);
重新编译并运行程序,输出结果如下:
主任张无忌审批采购单:10001,金额:45000.0元,采购目的:购买倚天剑。 经理黄蓉审批采购单:10002,金额:60000.0元,采购目的:购买《葵花宝典》。 董事长郭靖审批采购单:10003,金额:160000.0元,采购目的:购买《金刚经》。 召开董事会审批采购单:10004,金额:800000.0元,采购目的:购买桃花岛。 ```
思考
如果将审批流程由“主任-->副董事长-->董事长-->董事会”调整为“主任-->董事长-->董事会”,系统将做出哪些改动?预测修改之后客户端代码的输出结果。
16.4 纯与不纯的职责链模式
职责链模式可分为纯的职责链模式和不纯的职责链模式两种:
(1) 纯的职责链模式
一个纯的职责链模式要求一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况。而且在纯的职责链模式中,要求一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况。在前面的采购单审批实例中应用的是纯的职责链模式。
(2)不纯的职责链模式
在一个不纯的职责链模式中允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,而且一个请求可以最终不被任何处理者对象所接收。Java AWT 1.0中的事件处理模型应用的是不纯的职责链模式,其基本原理如下:由于窗口组件(如按钮、文本框等)一般都位于容器组件中,因此当事件发生在某一个组件上时,先通过组件对象的handleEvent()方法将事件传递给相应的事件处理方法,该事件处理方法将处理此事件,然后决定是否将该事件向上一级容器组件传播;上级容器组件在接到事件之后可以继续处理此事件并决定是否继续向上级容器组件传播,如此反复,直到事件到达顶层容器组件为止;如果一直传到最顶层容器仍没有处理方法,则该事件不予处理。每一级组件在接收到事件时,都可以处理此事件,而不论此事件是否在上一级已得到处理,还存在事件未被处理的情况。显然,这就是不纯的职责链模式,早期的Java AWT事件模型(JDK 1.0及更早)中的这种事件处理机制又叫事件浮升(Event Bubbling)机制。从Java.1.1以后,JDK使用观察者模式代替职责链模式来处理事件。目前,在JavaScript中仍然可以使用这种事件浮升机制来进行事件处理。
16.5 职责链模式总结
职责链模式通过建立一条链来组织请求的处理者,请求将沿着链进行传递,请求发送者无须知道请求在何时、何处以及如何被处理,实现了请求发送者与处理者的解耦。在软件开发中,如果遇到有多个对象可以处理同一请求时可以应用职责链模式,例如在Web应用开发中创建一个过滤器(Filter)链来对请求数据进行过滤,在工作流系统中实现公文的分级审批等等,使用职责链模式可以较好地解决此类问题。
1.主要优点
职责链模式的主要优点如下:
(1) 职责链模式使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。
(2) 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。
(3) 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责。
(4) 在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是符合“开闭原则”的。
2.主要缺点
职责链模式的主要缺点如下:
(1) 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理。
(2) 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便。
(3) 如果建链不当,可能会造成循环调用,将导致系统陷入死循环。
3.适用场景
在以下情况下可以考虑使用职责链模式:
(1) 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。
(2) 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
(3) 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。
练习
Sunny软件公司的OA系统需要提供一个假条审批模块:如果员工请假天数小于3天,主任可以审批该假条;如果员工请假天数大于等于3天,小于10天,经理可以审批;如果员工请假天数大于等于10天,小于30天,总经理可以审批;如果超过30天,总经理也不能审批,提示相应的拒绝信息。试用职责链模式设计该假条审批模块。
装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关闭,例如电灯或者排气扇。在购买开关时,我们并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、排气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制排气扇或者其他电器设备。开关与电器之间通过电线建立连接,如果开关打开,则电线通电,电器工作;反之,开关关闭,电线断电,电器停止工作。相同的开关可以通过不同的电线来控制不同的电器,如图1所示:
图1 开关与电灯、排气扇示意图
在图1中,我们可以将开关理解成一个请求的发送者,用户通过它来发送一个“开灯”请求,而电灯是“开灯”请求的最终接收者和处理者,在图中,开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者,只需更换一根电线,相同的发送者(开关)即可对应不同的接收者(电器)。
在软件开发中也存在很多与开关和电器类似的请求发送者和接收者对象,例如一个按钮,它可能是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接收者。为了降低系统的耦合度,将请求的发送者和接收者解耦,我们可以使用一种被称之为命令模式的设计模式来设计系统,在命令模式中,发送者与接收者之间引入了新的命令对象(类似图1中的电线),将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。本章我们将学习用于将请求发送者和接收者解耦的命令模式。
1 自定义功能键
Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。Sunny软件公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”,为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,该窗口界面如图2所示:
图2 “功能键设置”界面效果图
通过如图2所示界面,用户可以将功能键和相应功能绑定在一起,还可以根据需要来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。
Sunny软件公司某开发人员欲使用如下代码来实现功能键与功能处理类之间的调用关系:
//FunctionButton:功能键类,请求发送者
class FunctionButton {
private HelpHandler help; //HelpHandler:帮助文档处理类,请求接收者
//在FunctionButton的onClick()方法中调用HelpHandler的display()方法
publicvoid onClick() {
help = new HelpHandler();
help.display(); //显示帮助文档
}
}
在上述代码中,功能键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler充当请求的接收者,在发送者FunctionButton的onClick()方法中将调用接收者HelpHandler的display()方法。显然,如果使用上述代码,将给系统带来如下几个问题:
(1) 由于请求发送者和请求接收者之间存在方法的直接调用,耦合度很高,更换请求接收者必须修改发送者的源代码,如果需要将请求接收者HelpHandler改为WindowHanlder(窗口处理类),则需要修改FunctionButton的源代码,违背了“开闭原则”。
(2) FunctionButton类在设计和实现时功能已被固定,如果增加一个新的请求接收者,如果不修改原有的FunctionButton类,则必须增加一个新的与FunctionButton功能类似的类,这将导致系统中类的个数急剧增加。由于请求接收者HelpHandler、WindowHanlder等类之间可能不存在任何关系,它们没有共同的抽象层,因此也很难依据“依赖倒转原则”来设计FunctionButton。
(3) 用户无法按照自己的需要来设置某个功能键的功能,一个功能键类的功能一旦固定,在不修改源代码的情况下无法更换其功能,系统缺乏灵活性。
不难得知,所有这些问题的产生都是因为请求发送者FunctionButton类和请求接收者HelpHandler、WindowHanlder等类之间存在直接耦合关系,如何降低请求发送者和接收者之间的耦合度,让相同的发送者可以对应不同的接收者?这是Sunny软件公司开发人员在设计“功能键设置”模块时不得不考虑的问题。命令模式正为解决这类问题而诞生,此时,如果我们使用命令模式,可以在一定程度上解决上述问题(注:命令模式无法解决类的个数增加的问题),下面就让我们正式进入命令模式的学习,看看命令模式到底如何实现请求发送者和接收者解耦。
2 命令模式概述
在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式为此类问题提供了一个较为完美的解决方案。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
命令模式定义如下:
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
命令模式的定义比较复杂,提到了很多术语,例如“用不同的请求对客户进行参数化”、“对请求排队”,“记录请求日志”、“支持可撤销操作”等,在后面我们将对这些术语进行一一讲解。
命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如图3所示:
图3 命令模式结构图
在命令模式结构图中包含如下几个角色:
● Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
● ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
● Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
● Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。 典型的抽象命令类代码如下所示:
abstract class Command {
public abstract void execute();
}
对于请求发送者即调用者而言,将针对抽象命令类进行编程,可以通过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法,其典型代码如下所示:
class Invoker {
private Command command;
//构造注入
public Invoker(Command command) {
this.command = command;
}
//设值注入
public void setCommand(Command command) {
this.command = command;
}
//业务方法,用于调用命令类的execute()方法
public void call() {
command.execute();
}
}
具体命令类继承了抽象命令类,它与请求接收者相关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action(),其典型代码如下所示:
class ConcreteCommand extends Command{
private Receiver receiver; //维持一个对请求接收者对象的引用
public void execute() {
receiver.action(); //调用请求接收者的业务处理方法action()
}
}
请求接收者Receiver类具体实现对请求的业务处理,它提供了action()方法,用于执行与请求相关的操作,其典型代码如下所示:
class Receiver {
public void action() {
//具体操作
}
}
思考
一个请求发送者能否对应多个请求接收者?如何实现?
3 完整解决方案
为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能,Sunny软件公司开发人员使用命令模式来设计“自定义功能键”模块,其核心结构如图4所示:
图4 自定义功能键核心结构图
在图4中,FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类,WindowHanlder和HelpHandler充当请求接收者。完整代码如下所示:
import java.util.*;
//功能键设置窗口类
class FBSettingWindow {
private String title; //窗口标题
//定义一个ArrayList来存储所有功能键
private ArrayList
public FBSettingWindow(String title) {
this.title = title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return this.title;
}
public void addFunctionButton(FunctionButton fb) {
functionButtons.add(fb);
}
public void removeFunctionButton(FunctionButton fb) {
functionButtons.remove(fb);
}
//显示窗口及功能键
public void display() {
System.out.println("显示窗口:" +this.title);
System.out.println("显示功能键:");
for (Object obj : functionButtons) {
System.out.println(((FunctionButton)obj).getName());
}
System.out.println("------------------------------");
}
}
//功能键类:请求发送者
class FunctionButton {
private String name; //功能键名称
private Command command; //维持一个抽象命令对象的引用
public FunctionButton(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
//为功能键注入命令
public void setCommand(Command command) {
this.command = command;
}
//发送请求的方法
public void onClick() {
System.out.print("点击功能键:");
command.execute();
}
}
//抽象命令类
abstract class Command {
public abstract void execute();
}
//帮助命令类:具体命令类
class HelpCommand extends Command {
private HelpHandler hhObj; //维持对请求接收者的引用
public HelpCommand() {
hhObj = new HelpHandler();
}
//命令执行方法,将调用请求接收者的业务方法
public void execute() {
hhObj.display();
}
}
//最小化命令类:具体命令类
class MinimizeCommand extends Command{
private WindowHanlder whObj; //维持对请求接收者的引用
public MinimizeCommand() {
whObj = new WindowHanlder();
}
//命令执行方法,将调用请求接收者的业务方法
public void execute() {
whObj.minimize();
}
}
//窗口处理类:请求接收者
class WindowHanlder {
public void minimize() {
System.out.println("将窗口最小化至托盘!");
}
}
//帮助文档处理类:请求接收者
class HelpHandler {
public void display() {
System.out.println("显示帮助文档!");
}
}
为了提高系统的灵活性和可扩展性,我们将具体命令类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示:
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
public class XMLUtil {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象,可以通过参数的不同返回不同类名节点所对应的实例
public static Object getBean(int i) {
try {
//创建文档对象
DocumentBuilderFactory dFactory =DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));
//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode = null;
if (0 == i) {
classNode =nl.item(0).getFirstChild();
}
else {
classNode =nl.item(1).getFirstChild();
}
String cName = classNode.getNodeValue();
//通过类名生成实例对象并将其返回
Class c = Class.forName(cName);
Object obj = c.newInstance();
return obj;
}
catch(Exception e){
e.printStackTrace();
return null;
}
}
}
配置文件config.xml中存储了具体建造者类的类名,代码如下所示:
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
FBSettingWindow fbsw = new FBSettingWindow("功能键设置");
FunctionButton fb1,fb2;
fb1 = new FunctionButton("功能键1");
fb2 = new FunctionButton("功能键1");
Command command1,command2;
//通过读取配置文件和反射生成具体命令对象
command1 =(Command)XMLUtil.getBean(0);
command2 = (Command)XMLUtil.getBean(1);
//将命令对象注入功能键
fb1.setCommand(command1);
fb2.setCommand(command2);
fbsw.addFunctionButton(fb1);
fbsw.addFunctionButton(fb2);
fbsw.display();
//调用功能键的业务方法
fb1.onClick();
fb2.onClick();
}
}
编译并运行程序,输出结果如下:
显示窗口:功能键设置
显示功能键:
功能键1
功能键1
------------------------------
点击功能键:显示帮助文档!
点击功能键:将窗口最小化至托盘!
如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,原有代码无须修改,符合“开闭原则”。在此过程中,每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。
4 命令队列的实现
有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者,CommandQueue类的典型代码如下所示:
import java.util.*;
class CommandQueue {
//定义一个ArrayList来存储命令队列
private ArrayList
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
//循环调用每一个命令对象的execute()方法
public void execute() {
for (Object command : commands) {
((Command)command).execute();
}
}
}
在增加了命令队列类CommandQueue以后,请求发送者类Invoker将针对CommandQueue编程,代码修改如下:
class Invoker {
private CommandQueue commandQueue; //维持一个CommandQueue对象的引用
//构造注入
public Invoker(CommandQueue commandQueue) {
this. commandQueue = commandQueue;
}
//设值注入
public void setCommandQueue(CommandQueue commandQueue) {
this.commandQueue = commandQueue;
}
//调用CommandQueue类的execute()方法
public void call() {
commandQueue.execute();
}
}
命令队列与我们常说的“批处理”有点类似。批处理,顾名思义,可以对一组对象(命令)进行批量处理,当一个发送者发送请求后,将有一系列接收者对请求作出响应,命令队列可以用于设计批处理应用程序,如果请求接收者的接收次序没有严格的先后次序,我们还可以使用多线程技术来并发调用命令对象的execute()方法,从而提高程序的执行效率。
5 撤销操作的实现
在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。
扩展
除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现。
下面通过一个简单的实例来学习如何使用命令模式实现撤销操作:
Sunny软件公司欲开发一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。
Sunny软件公司开发人员使用命令模式设计了如图5所示结构图,其中计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,并且提供了可撤销加法运算的undo()方法。
图5 简易计算器结构图
本实例完整代码如下所示:
//加法类:请求接收者
class Adder {
private int num=0; //定义初始值为0
//加法操作,每次将传入的值与num作加法运算,再将结果返回
public int add(int value) {
num += value;
return num;
}
}
//抽象命令类
abstract class AbstractCommand {
public abstract int execute(int value); //声明命令执行方法execute()
public abstract int undo(); //声明撤销方法undo()
}
//具体命令类
class ConcreteCommand extendsAbstractCommand {
private Adder adder = new Adder();
private int value;
//实现抽象命令类中声明的execute()方法,调用加法类的加法操作
publicint execute(int value) {
this.value=value;
return adder.add(value);
}
//实现抽象命令类中声明的undo()方法,通过加一个相反数来实现加法的逆向操作
public int undo() {
return adder.add(-value);
}
}
//计算器界面类:请求发送者
class CalculatorForm {
private AbstractCommand command;
public void setCommand(AbstractCommand command) {
this.command = command;
}
//调用命令对象的execute()方法执行运算
public void compute(int value) {
int i = command.execute(value);
System.out.println("执行运算,运算结果为:" +i);
}
//调用命令对象的undo()方法执行撤销
public void undo() {
int i = command.undo();
System.out.println("执行撤销,运算结果为:" +i);
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
CalculatorForm form = new CalculatorForm();
AbstractCommand command;
command = new ConcreteCommand();
form.setCommand(command); //向发送者注入命令对象
form.compute(10);
form.compute(5);
form.compute(10);
form.undo();
}
}
编译并运行程序,输出结果如下:
执行运算,运算结果为:10
执行运算,运算结果为:15
执行运算,运算结果为:25
执行撤销,运算结果为:15
思考
如果连续调用“form.undo()”两次,预测客户端代码的输出结果。
需要注意的是在本实例中只能实现一步撤销操作,因为没有保存命令对象的历史状态,可以通过引入一个命令集合或其他方式来存储每一次操作时命令的状态,从而实现多次撤销操作。除了Undo操作外,还可以采用类似的方式实现恢复(Redo)操作,即恢复所撤销的操作(或称为二次撤销)。
练习
修改简易计算器源代码,使之能够实现多次撤销(Undo)和恢复(Redo)。
6 请求日志
请求日志就是将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中。很多系统都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件可以记录用户对系统的一些操作(例如对数据的更改)。请求日志文件可以实现很多功能,常用功能如下:
(1) “天有不测风云”,一旦系统发生故障,日志文件可以为系统提供一种恢复机制,在请求日志文件中可以记录用户对系统的每一步操作,从而让系统能够顺利恢复到某一个特定的状态;
(2) 请求日志也可以用于实现批处理,在一个请求日志文件中可以存储一系列命令对象,例如一个命令队列;
(3) 可以将命令队列中的所有命令对象都存储在一个日志文件中,每执行一个命令则从日志文件中删除一个对应的命令对象,防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行,只需读取请求日志文件,再继续执行文件中剩余的命令即可。
在实现请求日志时,我们可以将命令对象通过序列化写到日志文件中,此时命令类必须实现Java.io.Serializable接口。下面我们通过一个简单实例来说明日志文件的用途以及如何实现请求日志:
Sunny软件公司开发了一个网站配置文件管理工具,可以通过一个可视化界面对网站配置文件进行增删改等操作,该工具使用命令模式进行设计,结构如图6所示:
图6 网站配置文件管理工具结构图
现在Sunny软件公司开发人员希望将对配置文件的操作请求记录在日志文件中,如果网站重新部署,只需要执行保存在日志文件中的命令对象即可修改配置文件。
本实例完整代码如下所示:
import java.io.*;
import java.util.*;
//抽象命令类,由于需要将命令对象写入文件,因此它实现了Serializable接口
abstract class Command implementsSerializable {
protected String name; //命令名称
protected String args; //命令参数
protected ConfigOperator configOperator; //维持对接收者对象的引用
public Command(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public void setConfigOperator(ConfigOperator configOperator) {
this.configOperator = configOperator;
}
//声明两个抽象的执行方法execute()
public abstract void execute(String args);
public abstract void execute();
}
//增加命令类:具体命令
class InsertCommand extends Command {
public InsertCommand(String name) {
super(name);
}
public void execute(String args) {
this.args = args;
configOperator.insert(args);
}
public void execute() {
configOperator.insert(this.args);
}
}
//修改命令类:具体命令
class ModifyCommand extends Command {
public ModifyCommand(String name) {
super(name);
}
public void execute(String args) {
this.args = args;
configOperator.modify(args);
}
public void execute() {
configOperator.modify(this.args);
}
}
//省略了删除命令类DeleteCommand
//配置文件操作类:请求接收者。由于ConfigOperator类的对象是Command的成员对象,它也将随Command对象一起写入文件,因此ConfigOperator也需要实现Serializable接口
class ConfigOperator implementsSerializable {
public void insert(String args) {
System.out.println("增加新节点:" +args);
}
public void modify(String args) {
System.out.println("修改节点:" +args);
}
public void delete(String args) {
System.out.println("删除节点:" +args);
}
}
//配置文件设置窗口类:请求发送者
class ConfigSettingWindow {
//定义一个集合来存储每一次操作时的命令对象
private ArrayList
private Command command;
//注入具体命令对象
public void setCommand(Command command) {
this.command = command;
}
//执行配置文件修改命令,同时将命令对象添加到命令集合中
public void call(String args) {
command.execute(args);
commands.add(command);
}
//记录请求日志,生成日志文件,将命令集合写入日志文件
public void save() {
FileUtil.writeCommands(commands);
}
//从日志文件中提取命令集合,并循环调用每一个命令对象的execute()方法来实现配置文件的重新设置
public void recover() {
ArrayList list;
list = FileUtil.readCommands();
for (Object obj : list) {
((Command)obj).execute();
}
}
}
//工具类:文件操作类
class FileUtil {
//将命令集合写入日志文件
public static void writeCommands(ArrayList commands) {
try {
FileOutputStream file = newFileOutputStream("config.log");
//创建对象输出流用于将对象写入到文件中
ObjectOutputStream objout = new ObjectOutputStream(newBufferedOutputStream(file));
//将对象写入文件
objout.writeObject(commands);
objout.close();
}
catch(Exception e) {
System.out.println("命令保存失败!");
e.printStackTrace();
}
}
//从日志文件中提取命令集合
public static ArrayList readCommands() {
try {
FileInputStream file = new FileInputStream("config.log");
//创建对象输入流用于从文件中读取对象
ObjectInputStream objin = new ObjectInputStream(newBufferedInputStream(file));
//将文件中的对象读出并转换为ArrayList类型
ArrayList commands = (ArrayList)objin.readObject();
objin.close();
return commands;
}
catch(Exception e) {
System.out.println("命令读取失败!");
e.printStackTrace();
return null;
}
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
ConfigSettingWindow csw = new ConfigSettingWindow(); //定义请求发送者
Command command; //定义命令对象
ConfigOperator co = new ConfigOperator(); //定义请求接收者
//四次对配置文件的更改
command = new InsertCommand("增加");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("网站首页");
command = new InsertCommand("增加");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("端口号");
command = new ModifyCommand("修改");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("网站首页");
command = new ModifyCommand("修改");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("端口号");
System.out.println("----------------------------");
System.out.println("保存配置");
csw.save();
System.out.println("----------------------------");
System.out.println("恢复配置");
System.out.println("----------------------------");
csw.recover();
}
}
编译并运行程序,输出结果如下:
增加新节点:网站首页
增加新节点:端口号
修改节点:网站首页
修改节点:端口号
----------------------------
保存配置
----------------------------
恢复配置
----------------------------
增加新节点:网站首页
增加新节点:端口号
修改节点:网站首页
修改节点:端口号
7 宏命令
宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员可以是简单命令,还可以继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理,其结构如图7所示:
图7 宏命令结构图
8 命令模式总结
命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于GUI的软件开发,无论是在电脑桌面应用还是在移动应用中,命令模式都得到了广泛的应用。
主要优点
命令模式的主要优点如下:
(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
(3) 可以比较容易地设计一个命令队列或宏命令(组合命令)。
(4) 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
主要缺点
命令模式的主要缺点如下:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
适用场景
在以下情况下可以考虑使用命令模式:
(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
(3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
(4) 系统需要将一组操作组合在一起形成宏命令。
练习
Sunny软件公司欲开发一个基于Windows平台的公告板系统。该系统提供了一个主菜单(Menu),在主菜单中包含了一些菜单项(MenuItem),可以通过Menu类的addMenuItem()方法增加菜单项。菜单项的主要方法是click(),每一个菜单项包含一个抽象命令类,具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,命令类具有一个execute()方法,用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。试使用命令模式设计该系统,以便降低MenuItem类与BoardScreen类之间的耦合度。
虽然目前计算机编程语言有好几百种,但有时候我们还是希望能用一些简单的语言来实现一些特定的操作,我们只要向计算机输入一个句子或文件,它就能够按照预先定义的文法规则来对句子或文件进行解释,从而实现相应的功能。例如提供一个简单的加法/减法解释器,只要输入一个加法/减法表达式,它就能够计算出表达式结果,如图18-1所示,当输入字符串表达式为“1 + 2 + 3 – 4 + 1”时,将输出计算结果为3。
图18-1 加法/减法解释器示意图
我们知道,像C++、Java和C#等语言无法直接解释类似“1+ 2 + 3 – 4 + 1”这样的字符串(如果直接作为数值表达式时可以解释),我们必须自己定义一套文法规则来实现对这些语句的解释,即设计一个自定义语言。在实际开发中,这些简单的自定义语言可以基于现有的编程语言来设计,如果所基于的编程语言是面向对象语言,此时可以使用解释器模式来实现自定义语言。
18.1 机器人控制程序
Sunny软件公司欲为某玩具公司开发一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式也可以是复合表达式,每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中移动方向包括上(up)、下(down)、左(left)、右(right);移动方式包括移动(move)和快速移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。
用户通过对图形化的设置界面进行操作可以创建一个机器人控制指令,机器人在收到指令后将按照指令的设置进行移动,例如输入控制指令:up move 5,则“向上移动5个单位”;输入控制指令:down run 10 and left move 20,则“向下快速移动10个单位再向左移动20个单位”。
Sunny软件公司开发人员决定自定义一个简单的语言来解释机器人控制指令,根据上述需求描述,用形式化语言来表示该简单语言的文法规则如下:
expression ::= direction action distance |composite //表达式
composite ::= expression 'and' expression//复合表达式
direction ::= 'up' | 'down' | 'left' |'right' //移动方向
action ::= 'move' | 'run' //移动方式
distance ::= an integer //移动距离
上述语言一共定义了五条文法规则,对应五个语言单位,这些语言单位可以分为两类,一类为终结符(也称为终结符表达式),例如direction、action和distance,它们是语言的最小组成单位,不能再进行拆分;另一类为非终结符(也称为非终结符表达式),例如expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符。
我们根据上述规则定义出的语言可以构成很多语句,计算机程序将根据这些语句进行某种操作。为了实现对语句的解释,可以使用解释器模式,在解释器模式中每一个文法规则都将对应一个类,扩展、改变文法以及增加新的文法规则都很方便,下面就让我们正式进入解释器模式的学习,看看使用解释器模式如何来实现对机器人控制指令的处理。
18.2 文法规则和抽象语法树
解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。在正式分析解释器模式结构之前,我们先来学习如何表示一个语言的文法规则以及如何构造一棵抽象语法树。
在前面所提到的加法/减法解释器中,每一个输入表达式,例如“1 + 2 + 3 – 4 + 1”,都包含了三个语言单位,可以使用如下文法规则来定义:
expression ::= value | operation
operation ::= expression '+' expression |expression '-' expression
value ::= an integer //一个整数值
该文法规则包含三条语句,第一条表示表达式的组成方式,其中value和operation是后面两个语言单位的定义,每一条语句所定义的字符串如operation和value称为语言构造成分或语言单位,符号“::=”表示“定义为”的意思,其左边的语言单位通过右边来进行说明和定义,语言单位对应终结符表达式和非终结符表达式。如本规则中的operation是非终结符表达式,它的组成元素仍然可以是表达式,可以进一步分解,而value是终结符表达式,它的组成元素是最基本的语言单位,不能再进行分解。
在文法规则定义中可以使用一些符号来表示不同的含义,如使用“|”表示或,使用“{”和“}”表示组合,使用“*”表示出现0次或多次等,其中使用频率最高的符号是表示“或”关系的“|”,如文法规则“boolValue ::= 0 | 1”表示终结符表达式boolValue的取值可以为0或者1。
除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为抽象语法树(AbstractSyntax Tree, AST)的图形方式来直观地表示语言的构成,每一棵抽象语法树对应一个语言实例,如加法/减法表达式语言中的语句“1+ 2 + 3 – 4 + 1”,可以通过如图18-2所示抽象语法树来表示:
图18-2 抽象语法树示意图
在该抽象语法树中,可以通过终结符表达式value和非终结符表达式operation组成复杂的语句,每个文法规则的语言实例都可以表示为一个抽象语法树,即每一条具体的语句都可以用类似图18-2所示的抽象语法树来表示,在图中终结符表达式类的实例作为树的叶子节点,而非终结符表达式类的实例作为非叶子节点,它们可以将终结符表达式类的实例以及包含终结符和非终结符实例的子表达式作为其子节点。抽象语法树描述了如何构成一个复杂的句子,通过对抽象语法树的分析,可以识别出语言中的终结符类和非终结符类。
18.3 解释器模式概述
解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。在某些情况下,为了更好地描述某一些特定类型的问题,我们可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。此时,可以使用解释器模式来设计这种新的语言。对解释器模式的学习能够加深我们对面向对象思想的理解,并且掌握编程语言中文法规则的解释过程。
解释器模式定义如下: 解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。
由于表达式可分为终结符表达式和非终结符表达式,因此解释器模式的结构与组合模式的结构有些类似,但在解释器模式中包含更多的组成元素,它的结构如图18-3所示:
图18-3 解释器模式结构图
在解释器模式结构图中包含如下几个角色:
● AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
● TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
● NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
● Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类,其典型代码如下所示:
abstract class AbstractExpression {
public abstract voidinterpret(Context ctx);
}
终结符表达式和非终结符表达式类都是抽象表达式类的子类,对于终结符表达式,其代码很简单,主要是对终结符元素的处理,其典型代码如下所示:
class TerminalExpression extends AbstractExpression {
public void interpret(Contextctx) {
//终结符表达式的解释操作
}
}
对于非终结符表达式,其代码相对比较复杂,因为可以通过非终结符将表达式组合成更加复杂的结构,对于包含两个操作元素的非终结符表达式类,其典型代码如下:
class NonterminalExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public NonterminalExpression(AbstractExpression left,AbstractExpression right){
this.left=left;
this.right=right;
}
public void interpret(Context ctx) {
//递归调用每一个组成部分的interpret()方法
//在递归调用时指定组成部分的连接方式,即非终结符的功能
}
}
除了上述用于表示表达式的类以外,通常在解释器模式中还提供了一个环境类Context,用于存储一些全局信息,通常在Context中包含了一个HashMap或ArrayList等类型的集合对象(也可以直接由HashMap等集合类充当环境类),存储一系列公共信息,如变量名与值的映射关系(key/value)等,用于在进行具体的解释操作时从中获取相关信息。其典型代码片段如下:
class Context {
private HashMap map = new HashMap();
public void assign(String key, String value) {
//往环境类中设值
}
public String lookup(String key) {
//获取存储在环境类中的值
}
}
当系统无须提供全局公共信息时可以省略环境类,可根据实际情况决定是否需要环境类。
思考
绘制加法/减法解释器的类图并编写核心实现代码。
18.4 完整解决方案
为了能够解释机器人控制指令,Sunny软件公司开发人员使用解释器模式来设计和实现机器人控制程序。针对五条文法规则,分别提供五个类来实现,其中终结符表达式direction、action和distance对应DirectionNode类、ActionNode类和DistanceNode类,非终结符表达式expression和composite对应SentenceNode类和AndNode类。
我们可以通过抽象语法树来表示具体解释过程,例如机器人控制指令“down run 10and left move 20”对应的抽象语法树如图18-4所示:
图18-4 机器人控制程序抽象语法树实例
机器人控制程序实例基本结构如图18-5所示:
图18-5 机器人控制程序结构图
在图18-5中,AbstractNode充当抽象表达式角色,DirectionNode、ActionNode和DistanceNode充当终结符表达式角色,AndNode和SentenceNode充当非终结符表达式角色。完整代码如下所示:
//注:本实例对机器人控制指令的输出结果进行模拟,将英文指令翻译为中文指令,实际情况是调用不同的控制程序进行机器人的控制,包括对移动方向、方式和距离的控制等
import java.util.*;
//抽象表达式
abstract class AbstractNode {
public abstract String interpret();
}
//And解释:非终结符表达式
class AndNode extends AbstractNode {
private AbstractNode left; //And的左表达式
private AbstractNode right; //And的右表达式
public AndNode(AbstractNode left, AbstractNode right) {
this.left = left;
this.right = right;
}
//And表达式解释操作
public String interpret() {
return left.interpret() + "再" +right.interpret();
}
}
//简单句子解释:非终结符表达式
class SentenceNode extends AbstractNode{
private AbstractNode direction;
private AbstractNode action;
private AbstractNode distance;
public SentenceNode(AbstractNode direction,AbstractNodeaction,AbstractNode distance) {
this.direction = direction;
this.action = action;
this.distance = distance;
}
//简单句子的解释操作
public String interpret() {
return direction.interpret() + action.interpret() +distance.interpret();
}
}
//方向解释:终结符表达式
class DirectionNode extends AbstractNode{
private String direction;
public DirectionNode(String direction) {
this.direction = direction;
}
//方向表达式的解释操作
public String interpret() {
if (direction.equalsIgnoreCase("up")) {
return "向上";
}
else if (direction.equalsIgnoreCase("down")) {
return "向下";
}
else if (direction.equalsIgnoreCase("left")) {
return "向左";
}
else if (direction.equalsIgnoreCase("right")) {
return "向右";
}
else {
return "无效指令";
}
}
}
//动作解释:终结符表达式
class ActionNode extends AbstractNode{
private String action;
public ActionNode(String action) {
this.action = action;
}
//动作(移动方式)表达式的解释操作
public String interpret() {
if (action.equalsIgnoreCase("move")) {
return "移动";
}
else if (action.equalsIgnoreCase("run")) {
return "快速移动";
}
else {
return "无效指令";
}
}
}
//距离解释:终结符表达式
class DistanceNode extends AbstractNode{
private String distance;
public DistanceNode(String distance) {
this.distance = distance;
}
//距离表达式的解释操作
public String interpret() {
return this.distance;
}
}
//指令处理类:工具类
class InstructionHandler {
private String instruction;
private AbstractNode node;
public void handle(String instruction) {
AbstractNode left = null, right = null;
AbstractNode direction = null, action = null, distance = null;
Stack stack = new Stack(); //声明一个栈对象用于存储抽象语法树
String[] words = instruction.split(" "); //以空格分隔指令字符串
for (int i = 0; i < words.length; i++) {
//本实例采用栈的方式来处理指令,如果遇到“and”,则将其后的三个单词作为三个终结符表达式连成一个简单句子SentenceNode作为“and”的右表达式,而将从栈顶弹出的表达式作为“and”的左表达式,最后将新的“and”表达式压入栈中。 if(words[i].equalsIgnoreCase("and")) {
left = (AbstractNode)stack.pop(); //弹出栈顶表达式作为左表达式
String word1= words[++i];
direction = new DirectionNode(word1);
String word2 = words[++i];
action = new ActionNode(word2);
String word3 = words[++i];
distance = newDistanceNode(word3);
right = newSentenceNode(direction,action,distance); //右表达式
stack.push(newAndNode(left,right)); //将新表达式压入栈中
}
//如果是从头开始进行解释,则将前三个单词组成一个简单句子SentenceNode并将该句子压入栈中
else {
String word1 = words[i];
direction = new DirectionNode(word1);
String word2 = words[++i];
action = newActionNode(word2);
String word3 = words[++i];
distance = newDistanceNode(word3);
left = new SentenceNode(direction,action,distance);
stack.push(left); //将新表达式压入栈中
}
}
this.node = (AbstractNode)stack.pop(); //将全部表达式从栈中弹出
}
public String output() {
String result = node.interpret(); //解释表达式
return result;
}
}
工具类InstructionHandler用于对输入指令进行处理,将输入指令分割为字符串数组,将第1个、第2个和第3个单词组合成一个句子,并存入栈中;如果发现有单词“and”,则将“and”后的第1个、第2个和第3个单词组合成一个新的句子作为“and”的右表达式,并从栈中取出原先所存句子作为左表达式,然后组合成一个And节点存入栈中。依此类推,直到整个指令解析结束。
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
String instruction = "up move 5 and down run 10 and left move5";
InstructionHandler handler = new InstructionHandler();
handler.handle(instruction);
String outString;
outString =handler.output();
System.out.println(outString);
}
}
编译并运行程序,输出结果如下:
向上移动5再向下快速移动10再向左移动5
18.5 再谈Context的作用
在解释器模式中,环境类Context用于存储解释器之外的一些全局信息,它通常作为参数被传递到所有表达式的解释方法interpret()中,可以在Context对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据,此外还可以在Context中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。
在上面的机器人控制程序实例中,我们省略了环境类角色,下面再通过一个简单实例来说明环境类的用途:
Sunny软件公司开发了一套简单的基于字符界面的格式化指令,可以根据输入的指令在字符界面中输出一些格式化内容,例如输入“LOOP 2 PRINT杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT郭靖 SPACE SPACE PRINT 黄蓉”,将输出如下结果:
杨过 小龙女
杨过 小龙女
郭靖 黄蓉
其中关键词LOOP表示“循环”,后面的数字表示循环次数;PRINT表示“打印”,后面的字符串表示打印的内容;SPACE表示“空格”;BREAK表示“换行”;END表示“循环结束”。每一个关键词对应一条命令,计算机程序将根据关键词执行相应的处理操作。
现使用解释器模式设计并实现该格式化指令的解释,对指令进行分析并调用相应的操作执行指令中每一条命令。
Sunny软件公司开发人员通过分析,根据该格式化指令中句子的组成,定义了如下文法规则:
expression ::= command* //表达式,一个表达式包含多条命令
command ::= loop | primitive //语句命令
loop ::= 'loopnumber' expression 'end' //循环命令,其中number为自然数
primitive ::= 'printstring' | 'space' | 'break' //基本命令,其中string为字符串
根据以上文法规则,通过进一步分析,绘制如图18-6所示结构图:
图18-6 格式化指令结构图
在图18-6中,Context充当环境角色,Node充当抽象表达式角色,ExpressionNode、CommandNode和LoopCommandNode充当非终结符表达式角色,PrimitiveCommandNode充当终结符表达式角色。完整代码如下所示:
import java.util.*;
//环境类:用于存储和操作需要解释的语句,在本实例中每一个需要解释的单词可以称为一个动作标记(Action Token)或命令
class Context {
private StringTokenizer tokenizer; //StringTokenizer类,用于将字符串分解为更小的字符串标记(Token),默认情况下以空格作为分隔符
private String currentToken; //当前字符串标记
public Context(String text) {
tokenizer = new StringTokenizer(text); //通过传入的指令字符串创建StringTokenizer对象
nextToken();
}
//返回下一个标记
public String nextToken() {
if (tokenizer.hasMoreTokens()) {
currentToken = tokenizer.nextToken();
}
else {
currentToken = null;
}
return currentToken;
}
//返回当前的标记
public String currentToken() {
return currentToken;
}
//跳过一个标记
public void skipToken(String token) {
if (!token.equals(currentToken)) {
System.err.println("错误提示:" +currentToken + "解释错误!");
}
nextToken();
}
//如果当前的标记是一个数字,则返回对应的数值
public int currentNumber() {
int number = 0;
try{
number = Integer.parseInt(currentToken); //将字符串转换为整数
}
catch(NumberFormatException e) {
System.err.println("错误提示:" + e);
}
return number;
}
}
//抽象节点类:抽象表达式
abstract class Node {
public abstract void interpret(Context text); //声明一个方法用于解释语句
public abstract void execute(); //声明一个方法用于执行标记对应的命令
}
//表达式节点类:非终结符表达式
class ExpressionNode extends Node {
private ArrayList
public void interpret(Context context) {
//循环处理Context中的标记
while (true){
//如果已经没有任何标记,则退出解释
if (context.currentToken() == null) {
break;
}
//如果标记为END,则不解释END并结束本次解释过程,可以继续之后的解释
else if (context.currentToken().equals("END")) {
context.skipToken("END");
break;
}
//如果为其他标记,则解释标记并将其加入命令集合
else {
Node commandNode = newCommandNode();
commandNode.interpret(context);
list.add(commandNode);
}
}
}
//循环执行命令集合中的每一条命令
public void execute() {
Iterator iterator = list.iterator();
while (iterator.hasNext()){
((Node)iterator.next()).execute();
}
}
}
//语句命令节点类:非终结符表达式
class CommandNode extends Node {
private Node node;
public void interpret(Context context) {
//处理LOOP循环命令
if (context.currentToken().equals("LOOP")) {
node = new LoopCommandNode();
node.interpret(context);
}
//处理其他基本命令
else {
node = new PrimitiveCommandNode();
node.interpret(context);
}
}
public void execute() {
node.execute();
}
}
//循环命令节点类:非终结符表达式
class LoopCommandNode extends Node {
private int number; //循环次数
private Node commandNode; //循环语句中的表达式
//解释循环命令
public void interpret(Context context) {
context.skipToken("LOOP");
number = context.currentNumber();
context.nextToken();
commandNode = new ExpressionNode(); //循环语句中的表达式
commandNode.interpret(context);
}
public void execute() {
for (int i=0;i commandNode.execute(); } } //基本命令节点类:终结符表达式 class PrimitiveCommandNode extends Node{ private String name; privateString text; //解释基本命令 public void interpret(Context context) { name = context.currentToken(); context.skipToken(name); if (!name.equals("PRINT") &&!name.equals("BREAK") && !name.equals("SPACE")){ System.err.println("非法命令!"); } if (name.equals("PRINT")){ text = context.currentToken(); context.nextToken(); } } public void execute(){ if (name.equals("PRINT")) System.out.print(text); else if (name.equals("SPACE")) System.out.print(" "); else if (name.equals("BREAK")) System.out.println(); } } 在本实例代码中,环境类Context类似一个工具类,它提供了用于处理指令的方法,如nextToken()、currentToken()、skipToken()等,同时它存储了需要解释的指令并记录了每一次解释的当前标记(Token),而具体的解释过程交给表达式解释器类来处理。我们还可以将各种解释器类包含的公共方法移至环境类中,更好地实现这些方法的重用和扩展。 针对本实例代码,我们编写如下客户端测试代码: class Client{ public static void main(String[] args){ String text = "LOOP 2 PRINT 杨过 SPACE SPACEPRINT 小龙女 BREAK END PRINT 郭靖SPACE SPACE PRINT 黄蓉"; Context context = new Context(text); Node node = new ExpressionNode(); node.interpret(context); node.execute(); } } 编译并运行程序,输出结果如下: 杨过 小龙女 杨过 小龙女 郭靖 黄蓉 思考 预测指令“LOOP 2 LOOP 2 PRINT杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉 BREAK END”的输出结果。 18.6 解释器模式总结 解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java语言的语法结构,用户可以通过扩展其功能,创建自己的文法规则。 主要优点 解释器模式的主要优点如下: (1) 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。 (2) 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。 (3) 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。 (4) 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。 主要缺点 解释器模式的主要缺点如下: (1) 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。 (2) 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。 适用场景 在以下情况下可以考虑使用解释器模式: (1) 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 (2) 一些重复出现的问题可以用一种简单的语言来进行表达。 (3) 一个语言的文法较为简单。 (4) 执行效率不是关键问题。【注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。】 练习 Sunny软件公司欲为数据库备份和同步开发一套简单的数据库同步指令,通过指令可以对数据库中的数据和结构进行备份,例如,输入指令“COPY VIEW FROM srcDB TO desDB”表示将数据库srcDB中的所有视图(View)对象都拷贝至数据库desDB;输入指令“MOVE TABLE Student FROM srcDB TO desDB”表示将数据库srcDB中的Student表移动至数据库desDB。试使用解释器模式来设计并实现该数据库同步指令。 【注:本练习是2010年我在给某公司进行设计模式内训时该公司正在开发的一个小工具!】 20世纪80年代,那时我家有一台“古老的”电视机,牌子我忘了,只记得是台黑白电视机,没有遥控器,每次开关机或者换台都需要通过电视机上面的那些按钮来完成,我印象最深的是那个用来换台的按钮,需要亲自用手去旋转(还要使点劲才能拧动),每转一下就“啪”的响一声,如果没有收到任何电视频道就会出现一片让人眼花的雪花点。当然,电视机上面那两根可以前后左右移动,并能够变长变短的天线也是当年电视机的标志性部件之一,我记得小时候每次画电视机时一定要画那两根天线,要不总觉得不是电视机。随着科技的飞速发展,越来越高级的电视机相继出现,那种古老的电视机已经很少能够看到了。与那时的电视机相比,现今的电视机给我们带来的最大便利之一就是增加了电视机遥控器,我们在进行开机、关机、换台、改变音量等操作时都无须直接操作电视机,可以通过遥控器来间接实现。我们可以将电视机看成一个存储电视频道的集合对象,通过遥控器可以对电视机中的电视频道集合进行操作,如返回上一个频道、跳转到下一个频道或者跳转至指定的频道。遥控器为我们操作电视频道带来很大的方便,用户并不需要知道这些频道到底如何存储在电视机中。电视机遥控器和电视机示意图如图1所示: 图1 电视机遥控器与电视机示意图 在软件开发中,也存在大量类似电视机一样的类,它们可以存储多个成员对象(元素),这些类通常称为聚合类(Aggregate Classes),对应的对象称为聚合对象。为了更加方便地操作这些聚合对象,同时可以很灵活地为聚合对象增加不同的遍历方法,我们也需要类似电视机遥控器一样的角色,可以访问一个聚合对象中的元素但又不需要暴露它的内部结构。本章我们将要学习的迭代器模式将为聚合对象提供一个遥控器,通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。 1 销售管理系统中数据的遍历 Sunny软件公司为某商场开发了一套销售管理系统,在对该系统进行分析和设计时,Sunny软件公司开发人员发现经常需要对系统中的商品数据、客户数据等进行遍历,为了复用这些遍历代码,Sunny公司开发人员设计了一个抽象的数据集合类AbstractObjectList,而将存储商品和客户等数据的类作为其子类,AbstractObjectList类结构如图2所示: 图2 AbstractObjectList类结构图 在图2中,List类型的对象objects用于存储数据,方法说明如表1所示: 表1 AbstractObjectList类方法说明 AbstractObjectList类的子类ProductList和CustomerList分别用于存储商品数据和客户数据。 Sunny软件公司开发人员通过对AbstractObjectList类结构进行分析,发现该设计方案存在如下几个问题: (1) 在图2所示类图中,addObject()、removeObject()等方法用于管理数据,而next()、isLast()、previous()、isFirst()等方法用于遍历数据。这将导致聚合类的职责过重,它既负责存储和管理数据,又负责遍历数据,违反了“单一职责原则”,由于聚合类非常庞大,实现代码过长,还将给测试和维护增加难度。 (2) 如果将抽象聚合类声明为一个接口,则在这个接口中充斥着大量方法,不利于子类实现,违反了“接口隔离原则”。 (3) 如果将所有的遍历操作都交给子类来实现,将导致子类代码庞大,而且必须暴露AbstractObjectList的内部存储细节,向子类公开自己的私有属性,否则子类无法实施对数据的遍历,这将破坏AbstractObjectList类的封装性。 如何解决上述问题?解决方案之一就是将聚合类中负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,无须暴露聚合类的内部属性即可对其进行操作,而这正是迭代器模式的意图所在。 2 迭代器模式概述 在软件开发中,我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。 迭代器模式定义如下: 迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。 在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式,其模式结构如图3所示: 图3 迭代器模式结构图 在迭代器模式结构图中包含如下几个角色: ● Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。 ● ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。 ● Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。 ● ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。 在迭代器模式中,提供了一个外部的迭代器来对聚合对象进行访问和遍历,迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。迭代器的引入,将使得对一个复杂聚合对象的操作变得简单。 下面我们结合代码来对迭代器模式的结构进行进一步分析。在迭代器模式中应用了工厂方法模式,抽象迭代器对应于抽象产品角色,具体迭代器对应于具体产品角色,抽象聚合类对应于抽象工厂角色,具体聚合类对应于具体工厂角色。 在抽象迭代器中声明了用于遍历聚合对象中所存储元素的方法,典型代码如下所示: interface Iterator { public void first(); //将游标指向第一个元素 public void next(); //将游标指向下一个元素 public boolean hasNext(); //判断是否存在下一个元素 public Object currentItem(); //获取游标指向的当前元素 } 在具体迭代器中将实现抽象迭代器声明的遍历数据的方法,如下代码所示: class ConcreteIterator implements Iterator{ private ConcreteAggregate objects; //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据 private int cursor; //定义一个游标,用于记录当前访问位置 public ConcreteIterator(ConcreteAggregate objects) { this.objects=objects; } public void first() { ...... } public void next() { ...... } public boolean hasNext() { ...... } public Object currentItem() { ...... } } 需要注意的是抽象迭代器接口的设计非常重要,一方面需要充分满足各种遍历操作的要求,尽量为各种遍历方法都提供声明,另一方面又不能包含太多方法,接口中方法太多将给子类的实现带来麻烦。因此,可以考虑使用抽象类来设计抽象迭代器,在抽象类中为每一个方法提供一个空的默认实现。如果需要在具体迭代器中为聚合对象增加全新的遍历操作,则必须修改抽象迭代器和具体迭代器的源代码,这将违反“开闭原则”,因此在设计时要考虑全面,避免之后修改接口。 聚合类用于存储数据并负责创建迭代器对象,最简单的抽象聚合类代码如下所示: interface Aggregate { Iterator createIterator(); } 具体聚合类作为抽象聚合类的子类,一方面负责存储数据,另一方面实现了在抽象聚合类中声明的工厂方法createIterator(),用于返回一个与该具体聚合类对应的具体迭代器对象,代码如下所示: class ConcreteAggregate implementsAggregate { ...... public Iterator createIterator() { return new ConcreteIterator(this); } ...... } 思考 理解迭代器模式中具体聚合类与具体迭代器类之间存在的依赖关系和关联关系。 3 完整解决方案 为了简化AbstractObjectList类的结构,并给不同的具体数据集合类提供不同的遍历方式,Sunny软件公司开发人员使用迭代器模式来重构AbstractObjectList类的设计,重构之后的销售管理系统数据遍历结构如图4所示: 图4 销售管理系统数据遍历结构图 (注:为了简化类图和代码,本结构图中只提供一个具体聚合类和具体迭代器类) 在图4中,AbstractObjectList充当抽象聚合类,ProductList充当具体聚合类,AbstractIterator充当抽象迭代器,ProductIterator充当具体迭代器。完整代码如下所示: //在本实例中,为了详细说明自定义迭代器的实现过程,我们没有使用JDK中内置的迭代器,事实上,JDK内置迭代器已经实现了对一个List对象的正向遍历 import java.util.*; //抽象聚合类 abstract class AbstractObjectList { protected List public AbstractObjectList(List objects) { this.objects = objects; } public void addObject(Object obj) { this.objects.add(obj); } public void removeObject(Object obj) { this.objects.remove(obj); } public List getObjects() { return this.objects; } //声明创建迭代器对象的抽象工厂方法 public abstract AbstractIterator createIterator(); } //商品数据类:具体聚合类 class ProductList extendsAbstractObjectList { public ProductList(List products) { super(products); } //实现创建迭代器对象的具体工厂方法 public AbstractIterator createIterator() { return new ProductIterator(this); } } //抽象迭代器 interface AbstractIterator { public void next(); //移至下一个元素 public boolean isLast(); //判断是否为最后一个元素 public void previous(); //移至上一个元素 public boolean isFirst(); //判断是否为第一个元素 public Object getNextItem(); //获取下一个元素 public Object getPreviousItem(); //获取上一个元素 } //商品迭代器:具体迭代器 class ProductIterator implementsAbstractIterator { private ProductList productList; private List products; private int cursor1; //定义一个游标,用于记录正向遍历的位置 private int cursor2; //定义一个游标,用于记录逆向遍历的位置 public ProductIterator(ProductList list) { this.productList = list; this.products = list.getObjects(); //获取集合对象 cursor1 = 0; //设置正向遍历游标的初始值 cursor2 = products.size() -1; //设置逆向遍历游标的初始值 } public void next() { if(cursor1 < products.size()) { cursor1++; } } public boolean isLast() { return (cursor1 == products.size()); } public void previous() { if (cursor2 > -1) { cursor2--; } } public boolean isFirst() { return (cursor2 == -1); } public Object getNextItem() { return products.get(cursor1); } public Object getPreviousItem() { return products.get(cursor2); } } 编写如下客户端测试代码: class Client { public static void main(String args[]){ List products = new ArrayList(); products.add("倚天剑"); products.add("屠龙刀"); products.add("断肠草"); products.add("葵花宝典"); products.add("四十二章经"); AbstractObjectList list; AbstractIterator iterator; list = new ProductList(products); //创建聚合对象 iterator = list.createIterator(); //创建迭代器对象 System.out.println("正向遍历:"); while(!iterator.isLast()) { System.out.print(iterator.getNextItem() + ","); iterator.next(); } System.out.println(); System.out.println("-----------------------------"); System.out.println("逆向遍历:"); while(!iterator.isFirst()) { System.out.print(iterator.getPreviousItem() + ","); iterator.previous(); } } } 编译并运行程序,输出结果如下: 正向遍历: 倚天剑,屠龙刀,断肠草,葵花宝典,四十二章经, ----------------------------- 逆向遍历: 四十二章经,葵花宝典,断肠草,屠龙刀,倚天剑, 如果需要增加一个新的具体聚合类,如客户数据集合类,并且需要为客户数据集合类提供不同于商品数据集合类的正向遍历和逆向遍历操作,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合“开闭原则”;如果需要为ProductList类更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法,原有迭代器代码无须修改,也符合“开闭原则”;但是如果要在迭代器中增加新的方法,则需要修改抽象迭代器源代码,这将违背“开闭原则”。 4 使用内部类实现迭代器 在迭代器模式结构图中,我们可以看到具体迭代器类和具体聚合类之间存在双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便迭代器能够对这些数据进行遍历操作。 除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,我们还可以将迭代器类设计为聚合类的内部类,JDK中的迭代器类就是通过这种方法来实现的,如下AbstractList类代码片段所示: package java.util; …… public abstract class AbstractList ...... private class Itr implements Iterator int cursor = 0; ...... } …… } 我们可以通过类似的方法来设计第3节中的ProductList类,将ProductIterator类作为ProductList类的内部类,代码如下所示: //商品数据类:具体聚合类 class ProductList extendsAbstractObjectList { public ProductList(List products) { super(products); } public AbstractIterator createIterator() { return new ProductIterator(); } //商品迭代器:具体迭代器,内部类实现 private class ProductIterator implements AbstractIterator { private int cursor1; private int cursor2; public ProductIterator() { cursor1 = 0; cursor2 = objects.size() -1; } public void next() { if(cursor1 < objects.size()) { cursor1++; } } public boolean isLast() { return (cursor1 == objects.size()); } public void previous() { if(cursor2 > -1) { cursor2--; } } public boolean isFirst() { return (cursor2 == -1); } public Object getNextItem() { return objects.get(cursor1); } public Object getPreviousItem() { return objects.get(cursor2); } } } 无论使用哪种实现机制,客户端代码都是一样的,也就是说客户端无须关心具体迭代器对象的创建细节,只需通过调用工厂方法createIterator()即可得到一个可用的迭代器对象,这也是使用工厂方法模式的好处,通过工厂来封装对象的创建过程,简化了客户端的调用。 5 JDK内置迭代器 为了让开发人员能够更加方便地操作聚合对象,在Java、C#等编程语言中都提供了内置迭代器。在Java集合框架中,常用的List和Set等聚合类都继承(或实现)了java.util.Collection接口,在Collection接口中声明了如下方法(部分): package java.util; public interface Collection …… boolean add(Object c); boolean addAll(Collection c); boolean remove(Object o); boolean removeAll(Collection c); boolean remainAll(Collection c); Iterator iterator(); …… } 除了包含一些增加元素和删除元素的方法外,还提供了一个iterator()方法,用于返回一个Iterator迭代器对象,以便遍历聚合中的元素;具体的Java聚合类可以通过实现该iterator()方法返回一个具体的Iterator对象。 JDK中定义了抽象迭代器接口Iterator,代码如下所示: package java.util; public interface Iterator boolean hasNext(); E next(); void remove(); } 其中,hasNext()用于判断聚合对象中是否还存在下一个元素,为了不抛出异常,在每次调用next()之前需先调用hasNext(),如果有可供访问的元素,则返回true;next()方法用于将游标移至下一个元素,通过它可以逐个访问聚合中的元素,它返回游标所越过的那个元素的引用;remove()方法用于删除上次调用next()时所返回的元素。 Java迭代器工作原理如图5所示,在第一个next()方法被调用时,迭代器游标由“元素1”与“元素2”之间移至“元素2”与“元素3”之间,跨越了“元素2”,因此next()方法将返回对“元素2”的引用;在第二个next()方法被调用时,迭代器由“元素2”与“元素3”之间移至“元素3”和“元素4”之间,next()方法将返回对“元素3”的引用,如果此时调用remove()方法,即可将“元素3”删除。 图5 Java迭代器示意图 如下代码片段可用于删除聚合对象中的第一个元素: Iterator iterator =collection.iterator(); //collection是已实例化的聚合对象 iterator.next(); // 跳过第一个元素 iterator.remove(); // 删除第一个元素 需要注意的是,在这里,next()方法与remove()方法的调用是相互关联的。如果调用remove()之前,没有先对next()进行调用,那么将会抛出一个IllegalStateException异常,因为没有任何可供删除的元素。如下代码片段可用于删除两个相邻的元素: iterator.remove(); iterator.next(); //如果删除此行代码程序将抛异常 iterator.remove(); 在上面的代码片段中如果将代码iterator.next();去掉则程序运行抛异常,因为第二次删除时将找不到可供删除的元素。 在JDK中,Collection接口和Iterator接口充当了迭代器模式的抽象层,分别对应于抽象聚合类和抽象迭代器,而Collection接口的子类充当了具体聚合类,下面以List为例加以说明,图6列出了JDK中部分与List有关的类及它们之间的关系: 图6 Java集合框架中部分类结构图 (注:为了简化类图,本图省略了大量方法) 在JDK中,实际情况比图6要复杂很多,在图6中,List接口除了继承Collection接口的iterator()方法外,还增加了新的工厂方法listIterator(),专门用于创建ListIterator类型的迭代器,在List的子类LinkedList中实现了该方法,可用于创建具体的ListIterator子类ListItr的对象,代码如下所示: public ListIterator returnnew ListItr(index); } listIterator()方法用于返回具体迭代器ListItr类型的对象。在JDK源码中,AbstractList中的iterator()方法调用了listIterator()方法,如下代码所示: public Iterator iterator() { returnlistIterator(); } 客户端通过调用LinkedList类的iterator()方法,即可得到一个专门用于遍历LinkedList的迭代器对象。 大家可能会问?既然有了iterator()方法,为什么还要提供一个listIterator()方法呢?这两个方法的功能不会存在重复吗?干嘛要多此一举? 这是一个好问题。我给大家简单解释一下为什么要这样设计:由于在Iterator接口中定义的方法太少,只有三个,通过这三个方法只能实现正向遍历,而有时候我们需要对一个聚合对象进行逆向遍历等操作,因此在JDK的ListIterator接口中声明了用于逆向遍历的hasPrevious()和previous()等方法,如果客户端需要调用这两个方法来实现逆向遍历,就不能再使用iterator()方法来创建迭代器了,因为此时创建的迭代器对象是不具有这两个方法的。我们只能通过如下代码来创建ListIterator类型的迭代器对象: ListIterator i = c.listIterator(); 正因为如此,在JDK的List接口中不得不增加对listIterator()方法的声明,该方法可以返回一个ListIterator类型的迭代器,ListIterator迭代器具有更加强大的功能。 思考 为什么使用iterator()方法创建的迭代器无法实现逆向遍历? 在Java语言中,我们可以直接使用JDK内置的迭代器来遍历聚合对象中的元素,下面的代码演示了如何使用Java内置的迭代器: import java.util.*; class IteratorDemo { public static void process(Collection c) { Iteratori = c.iterator(); //创建迭代器对象 //通过迭代器遍历聚合对象 while(i.hasNext()) { System.out.println(i.next().toString()); } } public static void main(String args[]){ Collection persons; persons= new ArrayList(); //创建一个ArrayList类型的聚合对象 persons.add("张无忌"); persons.add("小龙女"); persons.add("令狐冲"); persons.add("韦小宝"); persons.add("袁紫衣"); persons.add("小龙女"); process(persons); } } 在静态方法process()中使用迭代器Iterator对Collection对象进行处理,该代码运行结果如下: 张无忌 小龙女 令狐冲 韦小宝 袁紫衣 小龙女 如果需要更换聚合类型,如将List改成Set,则只需更换具体聚合类类名,如将上述代码中的ArrayList改为HashSet,则输出结果如下: 令狐冲 张无忌 韦小宝 小龙女 袁紫衣 ``` 在HashSet中合并了重复元素,并且元素以随机次序输出,其结果与使用ArrayList不相同。由此可见,通过使用迭代器模式,使得更换具体聚合类变得非常方便,而且还可以根据需要增加新的聚合类,新的聚合类只需要实现Collection接口,无须修改原有类库代码,符合“开闭原则”。 6 迭代器模式总结 迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。 主要优点 迭代器模式的主要优点如下: (1) 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。 (2) 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。 (3) 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。 主要缺点 迭代器模式的主要缺点如下: (1) 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。 (2) 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器Iterator就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。 适用场景 在以下情况下可以考虑使用迭代器模式: (1) 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。 (2) 需要为一个聚合对象提供多种遍历方式。 (3) 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。 练习 设计一个逐页迭代器,每次可返回指定个数(一页)元素,并将该迭代器用于对数据进行分页处理。 练习 在Sunny软件公司开发的某教务管理系统中,一个班级(Class in School)包含多个学生(Student),使用Java内置迭代器实现对学生信息的遍历,要求按学生年龄由大到小的次序输出学生信息。 腾讯公司推出的QQ作为一款免费的即时聊天软件深受广大用户的喜爱,它已经成为很多人学习、工作和生活的一部分(不要告诉我你没有QQ哦)。在QQ聊天中,一般有两种聊天方式:第一种是用户与用户直接聊天,第二种是通过QQ群聊天,如图20-1所示。如果我们使用图20-1(A)所示方式,一个用户如果要与别的用户聊天或发送文件,通常需要加其他用户为好友,用户与用户之间存在多对多的联系,这将导致系统中用户之间的关系非常复杂,一个用户如果要将相同的信息或文件发送给其他所有用户,必须一个一个的发送,于是QQ群产生了,如图20-1(B)所示,如果使用QQ群,一个用户就可以向多个用户发送相同的信息和文件而无须一一进行发送,只需要将信息或文件发送到群中或作为群共享即可,群的作用就是将发送者所发送的信息和文件转发给每一个接收者用户。通过引入群的机制,将极大减少系统中用户之间的两两通信,用户与用户之间的联系可以通过群来实现。 图20-1 QQ聊天示意图 在有些软件中,某些类/对象之间的相互调用关系错综复杂,类似QQ用户之间的关系,此时,我们特别需要一个类似“QQ群”一样的中间类来协调这些类/对象之间的复杂关系,以降低系统的耦合度。有一个设计模式正为此而诞生,它就是本章将要介绍的中介者模式。 20.1 客户信息管理窗口的初始设计 Sunny软件公司欲开发一套CRM系统,其中包含一个客户信息管理模块,所设计的“客户信息管理窗口”界面效果图如图20-2所示: 图20-2 “客户信息管理窗口”界面图 Sunny公司开发人员通过分析发现,在图20-2中,界面组件之间存在较为复杂的交互关系:如果删除一个客户,要在客户列表(List)中删掉对应的项,客户选择组合框(ComboBox)中客户名称也将减少一个;如果增加一个客户信息,客户列表中需增加一个客户,且组合框中也将增加一项。 如果实现界面组件之间的交互是Sunny公司开发人员必须面对的一个问题? Sunny公司开发人员对组件之间的交互关系进行了分析,结果如下: (1) 当用户单击“增加”按钮、“删除”按钮、“修改”按钮或“查询”按钮时,界面左侧的“客户选择组合框”、“客户列表”以及界面中的文本框将产生响应。 (2) 当用户通过“客户选择组合框”选中某个客户姓名时,“客户列表”和文本框将产生响应。 (3) 当用户通过“客户列表”选中某个客户姓名时,“客户选择组合框”和文本框将产生响应。 于是,Sunny公司开发人员根据组件之间的交互关系绘制了如图20-3所示初始类图: 图20-3 “客户信息管理窗口”原始类图 与类图20-3所对应的框架代码片段如下: //按钮类 class Button { private List list; private ComboBox cb; private TextBox tb; ...... //界面组件的交互 public void change() { list.update(); cb.update(); tb.update(); } public void update() { ...... } ...... } //列表框类 class List { private ComboBox cb; private TextBox tb; ...... //界面组件的交互 public void change() { cb.update(); tb.update(); } public void update() { ...... } ...... } //组合框类 class ComboBox { private List list; private TextBox tb; ...... //界面组件的交互 public void change() { list.update(); tb.update(); } public void update() { ...... } ...... } //文本框类 class TextBox { public void update() { ...... } ...... } 分析图20-3所示初始结构图和上述代码,我们不难发现该设计方案存在如下问题: (1) 系统结构复杂且耦合度高:每一个界面组件都与多个其他组件之间产生相互关联和调用,若一个界面组件对象发生变化,需要跟踪与之有关联的其他所有组件并进行处理,系统组件之间呈现一种较为复杂的网状结构,组件之间的耦合度高。 (2) 组件的可重用性差:由于每一个组件和其他组件之间都具有很强的关联,若没有其他组件的支持,一个组件很难被另一个系统或模块重用,这些组件表现出来更像一个不可分割的整体,而在实际使用时,我们往往需要每一个组件都能够单独重用,而不是重用一个由多个组件组成的复杂结构。 (3) 系统的可扩展性差:如果在上述系统中增加一个新的组件类,则必须修改与之交互的其他组件类的源代码,将导致多个类的源代码需要修改,同样,如果要删除一个组件也存在类似的问题,这违反了“开闭原则”,可扩展性和灵活性欠佳。 由于存在上述问题,Sunny公司开发人员不得不对原有系统进行重构,那如何重构呢?大家想到了“迪米特法则”,引入一个“第三者”来降低现有系统中类之间的耦合度。由这个“第三者”来封装并协调原有组件两两之间复杂的引用关系,使之成为一个松耦合的系统,这个“第三者”又称为“中介者”,中介者模式因此而得名。下面让我们正式进入中介者模式的学习,学会如何使用中介者类来协调多个类/对象之间的交互,以达到降低系统耦合度的目的。 20.2 中介者模式概述 如果在一个系统中对象之间的联系呈现为网状结构,如图20-4所示。对象之间存在大量的多对多联系,将导致系统非常复杂,这些对象既会影响别的对象,也会被别的对象所影响,这些对象称为同事对象,它们之间通过彼此的相互作用实现系统的行为。在网状结构中,几乎每个对象都需要与其他对象发生相互作用,而这种相互作用表现为一个对象与另外一个对象的直接耦合,这将导致一个过度耦合的系统。 图20-4 对象之间存在复杂关系的网状结构 中介者模式可以使对象之间的关系数量急剧减少,通过引入中介者对象,可以将系统的网状结构变成以中介者为中心的星形结构,如图20-5所示。在这个星形结构中,同事对象不再直接与另一个对象联系,它通过中介者对象与另一个对象发生相互作用。中介者对象的存在保证了对象结构上的稳定,也就是说,系统的结构不会因为新对象的引入带来大量的修改工作。 图20-5 引入中介者对象的星型结构 如果在一个系统中对象之间存在多对多的相互关系,我们可以将对象之间的一些交互行为从各个对象中分离出来,并集中封装在一个中介者对象中,并由该中介者进行统一协调,这样对象之间多对多的复杂关系就转化为相对简单的一对多关系。通过引入中介者来简化对象之间的复杂交互,中介者模式是“迪米特法则”的一个典型应用。 中介者模式定义如下: 中介者模式(Mediator Pattern):用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。 在中介者模式中,我们引入了用于协调其他对象/类之间相互调用的中介者类,为了让系统具有更好的灵活性和可扩展性,通常还提供了抽象中介者,其结构图如图20-6所示: 图20-6 中介者模式结构图 在中介者模式结构图中包含如下几个角色: ● Mediator(抽象中介者):它定义一个接口,该接口用于与各同事对象之间进行通信。 ● ConcreteMediator(具体中介者):它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,它维持了对各个同事对象的引用。 ● Colleague(抽象同事类):它定义各个同事类公有的方法,并声明了一些抽象方法来供子类实现,同时它维持了一个对抽象中介者类的引用,其子类可以通过该引用来与中介者通信。 ● ConcreteColleague(具体同事类):它是抽象同事类的子类;每一个同事对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中声明的抽象方法。 中介者模式的核心在于中介者类的引入,在中介者模式中,中介者类承担了两方面的职责: (1) 中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,可通过中介者来实现间接调用。该中转作用属于中介者在结构上的支持。 (2) 协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致的和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。 在中介者模式中,典型的抽象中介者类代码如下所示: abstract class Mediator { protected ArrayList //注册方法,用于增加同事对象 public void register(Colleague colleague) { colleagues.add(colleague); } //声明抽象的业务方法 public abstract void operation(); } 在抽象中介者中可以定义一个同事类的集合,用于存储同事对象并提供注册方法,同时声明了具体中介者类所具有的方法。在具体中介者类中将实现这些抽象方法,典型的具体中介者类代码如下所示: class ConcreteMediator extends Mediator{ //实现业务方法,封装同事之间的调用 public void operation() { ...... ((Colleague)(colleagues.get(0))).method1(); //通过中介者调用同事类的方法 ...... } } 在具体中介者类中将调用同事类的方法,调用时可以增加一些自己的业务代码对调用进行控制。 在抽象同事类中维持了一个抽象中介者的引用,用于调用中介者的方法,典型的抽象同事类代码如下所示: abstract class Colleague { protected Mediator mediator; //维持一个抽象中介者的引用 public Colleague(Mediator mediator) { this.mediator=mediator; } public abstract void method1(); //声明自身方法,处理自己的行为 //定义依赖方法,与中介者进行通信 public void method2() { mediator.operation(); } } 在抽象同事类中声明了同事类的抽象方法,而在具体同事类中将实现这些方法,典型的具体同事类代码如下所示: class ConcreteColleague extends Colleague{ public ConcreteColleague(Mediator mediator) { super(mediator); } //实现自身方法 public void method1() { ...... } } 在具体同事类ConcreteColleague中实现了在抽象同事类中声明的方法,其中方法method1()是同事类的自身方法(Self-Method),用于处理自己的行为,而方法method2()是依赖方法(Depend-Method),用于调用在中介者中定义的方法,依赖中介者来完成相应的行为,例如调用另一个同事类的相关方法。 思考 如何理解同事类中的自身方法与依赖方法? 20.3 完整解决方案 为了协调界面组件对象之间的复杂交互关系,Sunny公司开发人员使用中介者模式来设计客户信息管理窗口,其结构示意图如图20-7所示: 图20-7 引入了中介者类的“客户信息管理窗口”结构示意图 图20-7只是一个重构之后的结构示意图,在具体实现时,为了确保系统具有更好的灵活性和可扩展性,我们需要定义抽象中介者和抽象组件类,其中抽象组件类是所有具体组件类的公共父类,完整类图如图20-8所示: 图20-8 重构后的“客户信息管理窗口”结构图 在图20-8中,Component充当抽象同事类,Button、List、ComboBox和TextBox充当具体同事类,Mediator充当抽象中介者类,ConcreteMediator充当具体中介者类,ConcreteMediator维持了对具体同事类的引用,为了简化ConcreteMediator类的代码,我们在其中只定义了一个Button对象和一个TextBox对象。完整代码如下所示: //抽象中介者 abstract class Mediator { public abstract void componentChanged(Component c); } //具体中介者 class ConcreteMediator extends Mediator{ //维持对各个同事对象的引用 public Button addButton; public List list; public TextBox userNameTextBox; public ComboBox cb; //封装同事对象之间的交互 publicvoid componentChanged(Component c) { //单击按钮 if(c== addButton) { System.out.println("--单击增加按钮--"); list.update(); cb.update(); userNameTextBox.update(); } //从列表框选择客户 else if(c == list) { System.out.println("--从列表框选择客户--"); cb.select(); userNameTextBox.setText(); } //从组合框选择客户 else if(c == cb) { System.out.println("--从组合框选择客户--"); cb.select(); userNameTextBox.setText(); } } } //抽象组件类:抽象同事类 abstract class Component { protected Mediator mediator; public void setMediator(Mediator mediator) { this.mediator = mediator; } //转发调用 public void changed() { mediator.componentChanged(this); } public abstract void update(); } //按钮类:具体同事类 class Button extends Component { public void update() { //按钮不产生交互 } } //列表框类:具体同事类 class List extends Component { public void update() { System.out.println("列表框增加一项:张无忌。"); } public void select() { System.out.println("列表框选中项:小龙女。"); } } //组合框类:具体同事类 class ComboBox extends Component { public void update() { System.out.println("组合框增加一项:张无忌。"); } public void select() { System.out.println("组合框选中项:小龙女。"); } } //文本框类:具体同事类 class TextBox extends Component { public void update() { System.out.println("客户信息增加成功后文本框清空。"); } public void setText() { System.out.println("文本框显示:小龙女。"); } } 编写如下客户端测试代码: class Client { public static void main(String args[]) { //定义中介者对象 ConcreteMediator mediator; mediator = new ConcreteMediator(); //定义同事对象 Button addBT = new Button(); List list = new List(); ComboBox cb = new ComboBox(); TextBox userNameTB = new TextBox(); addBT.setMediator(mediator); list.setMediator(mediator); cb.setMediator(mediator); userNameTB.setMediator(mediator); mediator.addButton = addBT; mediator.list = list; mediator.cb = cb; mediator.userNameTextBox = userNameTB; addBT.changed(); System.out.println("-----------------------------"); list.changed(); } } 编译并运行程序,输出结果如下: --单击增加按钮-- 列表框增加一项:张无忌。 组合框增加一项:张无忌。 客户信息增加成功后文本框清空。 ----------------------------- --从列表框选择客户-- 组合框选中项:小龙女。 文本框显示:小龙女。 20.4 中介者与同事类的扩展 Sunny软件公司CRM系统的客户对“客户信息管理窗口”提出了一个修改意见:要求在窗口的下端能够及时显示当前系统中客户信息的总数。修改之后的界面如图20-9所示: 图20-9 修改之后的“客户信息管理窗口”界面图 从图20-9中我们不难发现,可以通过增加一个文本标签(Label)来显示客户信息总数,而且当用户点击“增加”按钮或者“删除”按钮时,将改变文本标签的内容。 由于使用了中介者模式,在原有系统中增加新的组件(即新的同事类)将变得很容易,我们至少有如下两种解决方案: 【解决方案一】增加一个界面组件类Label,修改原有的具体中介者类ConcreteMediator,增加一个对Label对象的引用,然后修改componentChanged()方法中其他相关组件对象的业务处理代码,原有组件类无须任何修改,客户端代码也需针对新增组件Label进行适当修改。 【解决方案二】与方案一相同,首先增加一个Label类,但不修改原有具体中介者类ConcreteMediator的代码,而是增加一个ConcreteMediator的子类SubConcreteMediator来实现对Label对象的引用,然后在新增的中介者类SubConcreteMediator中通过覆盖componentChanged()方法来实现所有组件(包括新增Label组件)之间的交互,同样,原有组件类无须做任何修改,客户端代码需少许修改。 引入Label之后“客户信息管理窗口”类结构示意图如图20-10所示: 图20-10 增加Label组件类后的“客户信息管理窗口”结构示意图 由于【解决方案二】无须修改ConcreteMediator类,更符合“开闭原则”,因此我们选择该解决方案来对新增Label类进行处理,对应的完整类图如图20-11所示: 图20-11 修改之后的“客户信息管理窗口”结构图 在图20-11中,新增了具体同事类Label和具体中介者类SubConcreteMediator,代码如下所示: //文本标签类:具体同事类 class Label extends Component { public void update() { System.out.println("文本标签内容改变,客户信息总数加1。"); } } //新增具体中介者类 class SubConcreteMediator extendsConcreteMediator { //增加对Label对象的引用 public Label label; public void componentChanged(Component c) { //单击按钮 if(c== addButton) { System.out.println("--单击增加按钮--"); list.update(); cb.update(); userNameTextBox.update(); label.update(); //文本标签更新 } //从列表框选择客户 else if(c == list) { System.out.println("--从列表框选择客户--"); cb.select(); userNameTextBox.setText(); } //从组合框选择客户 else if(c == cb) { System.out.println("--从组合框选择客户--"); cb.select(); userNameTextBox.setText(); } } } 修改客户端测试代码: class Client { public static void main(String args[]) { //用新增具体中介者定义中介者对象 SubConcreteMediator mediator; mediator = new SubConcreteMediator(); Button addBT = new Button(); List list = new List(); ComboBox cb = new ComboBox(); TextBox userNameTB = new TextBox(); Label label = new Label(); addBT.setMediator(mediator); list.setMediator(mediator); cb.setMediator(mediator); userNameTB.setMediator(mediator); label.setMediator(mediator); mediator.addButton = addBT; mediator.list = list; mediator.cb = cb; mediator.userNameTextBox = userNameTB; mediator.label = label; addBT.changed(); System.out.println("-----------------------------"); list.changed(); } } 编译并运行程序,输出结果如下: --单击增加按钮-- 列表框增加一项:张无忌。 组合框增加一项:张无忌。 客户信息增加成功后文本框清空。 文本标签内容改变,客户信息总数加1。 ----------------------------- --从列表框选择客户-- 组合框选中项:小龙女。 文本框显示:小龙女。 由于在本实例中不同的组件类(即不同的同事类)所拥有的方法并不完全相同,因此中介者类没有针对抽象同事类编程,导致在具体中介者类中需要维持对具体同事类的引用,客户端代码无法完全透明地对待所有同事类和中介者类。在某些情况下,如果设计得当,可以在客户端透明地对同事类和中介者类编程,这样系统将具有更好的灵活性和可扩展性。 思考 如果不使用中介者模式,按照图20-3所示设计方案,增加新组件时原有系统该如何修改? 在中介者模式的实际使用过程中,如果需要引入新的具体同事类,只需要继承抽象同事类并实现其中的方法即可,由于具体同事类之间并无直接的引用关系,因此原有所有同事类无须进行任何修改,它们与新增同事对象之间的交互可以通过修改或者增加具体中介者类来实现;如果需要在原有系统中增加新的具体中介者类,只需要继承抽象中介者类(或已有的具体中介者类)并覆盖其中定义的方法即可,在新的具体中介者中可以通过不同的方式来处理对象之间的交互,也可以增加对新增同事的引用和调用。在客户端中只需要修改少许代码(如果引入配置文件的话有时可以不修改任何代码)就可以实现中介者的更换。 20.4 中介者模式总结 中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。中介者模式在事件驱动类软件中应用较为广泛,特别是基于GUI(Graphical User Interface,图形用户界面)的应用软件,此外,在类与类之间存在错综复杂的关联关系的系统中,中介者模式都能得到较好的应用。 主要优点 中介者模式的主要优点如下: (1) 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。 (2) 中介者模式可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合“开闭原则”。 (3) 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展。 主要缺点 中介者模式的主要缺点如下: 在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。 适用场景 在以下情况下可以考虑使用中介者模式: (1) 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。 (2) 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。 (3) 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的具体中介者类。 练习 Sunny软件公司欲开发一套图形界面类库。该类库需要包含若干预定义的窗格(Pane)对象,例如TextPane、ListPane、GraphicPane等,窗格之间不允许直接引用。基于该类库的应用由一个包含一组窗格的窗口(Window)组成,窗口需要协调窗格之间的行为。试采用中介者模式设计该系统。 每个人都有过后悔的时候,但人生并无后悔药,有些错误一旦发生就无法再挽回,有些人一旦错过就不会再回来,有些话一旦说出口就不可能再收回,这就是人生。为了不后悔,凡事我们都需要三思而后行。说了这么多,大家可能已经晕了,不是在学设计模式吗?为什么弄出这么一堆人生感悟来,呵呵,别着急,本章将介绍一种让我们可以在软件中实现后悔机制的设计模式——备忘录模式,它是软件中的“后悔药”,是软件中的“月光宝盒”。话不多说,下面就让我们进入备忘录模式的学习。 21.1 可悔棋的中国象棋 Sunny软件公司欲开发一款可以运行在Android平台的触摸式中国象棋软件,由于考虑到有些用户是“菜鸟”,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,用户走错棋或操作失误后可恢复到前一个步骤。如图21-1所示: 图21-1 Android版中国象棋软件界面示意图 如何实现“悔棋”功能是Sunny软件公司开发人员需要面对的一个重要问题,“悔棋”就是让系统恢复到某个历史状态,在很多软件中通常称之为“撤销”。下面我们来简单分析一下撤销功能的实现原理: 在实现撤销时,首先必须保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态。如图21-2所示: 图21-2撤销功能示意图 备忘录模式正为解决此类撤销问题而诞生,它为我们的软件提供了“后悔药”,通过使用备忘录模式可以使系统恢复到某一特定的历史状态。 21.2 备忘录模式概述 备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,当前很多软件都提供了撤销(Undo)操作,其中就使用了备忘录模式。 备忘录模式定义如下: 备忘录模式(Memento Pattern):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。 备忘录模式的核心是备忘录类以及用于管理备忘录的负责人类的设计,其结构如图21-3所示: 在备忘录模式结构图中包含如下几个角色: ● Originator(原发器):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。 ●Memento(备忘录):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。 ●Caretaker(负责人):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。 理解备忘录模式并不难,但关键在于如何设计备忘录类和负责人类。由于在备忘录中存储的是原发器的中间状态,因此需要防止原发器以外的其他对象访问备忘录,特别是不允许其他对象来修改备忘录。 下面我们通过简单的示例代码来说明如何使用Java语言实现备忘录模式: 在使用备忘录模式时,首先应该存在一个原发器类Originator,在真实业务中,原发器类是一个具体的业务类,它包含一些用于存储成员数据的属性,典型代码如下所示: package dp.memento; public class Originator { private String state; public Originator(){} // 创建一个备忘录对象 public Memento createMemento() { return new Memento(this); } // 根据备忘录对象恢复原发器状态 public void restoreMemento(Memento m) { state = m.state; } public void setState(String state) { this.state=state; } public String getState() { return this.state; } } 对于备忘录类Memento而言,它通常提供了与原发器相对应的属性(可以是全部,也可以是部分)用于存储原发器的状态,典型的备忘录类设计代码如下: package dp.memento; //备忘录类,默认可见性,包内可见 class Memento { private String state; public Memento(Originator o) { state = o.getState(); } public void setState(String state) { this.state=state; } public String getState() { return this.state; } } 在设计备忘录类时需要考虑其封装性,除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法,如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。 在使用Java语言实现备忘录模式时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认访问标识符来定义Memento类,即保证其包内可见。只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。在 Memento中保存了Originator的state值,如果Originator中的state值改变之后需撤销,可以通过调用它的restoreMemento()方法进行恢复。 对于负责人类Caretaker,它用于保存备忘录对象,并提供getMemento()方法用于向客户端返回一个备忘录对象,原发器通过使用这个备忘录对象可以回到某个历史状态。典型的负责人类的实现代码如下: package dp.memento; public class Caretaker { private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento=memento; } } 在Caretaker类中不应该直接调用Memento中的状态改变方法,它的作用仅仅用于存储备忘录对象。将原发器备份生成的备忘录对象存储在其中,当用户需要对原发器进行恢复时再将存储在其中的备忘录对象取出。 思考 能否通过原型模式来创建备忘录对象?系统该如何设计? 21.3 完整解决方案 为了实现撤销功能,Sunny公司开发人员决定使用备忘录模式来设计中国象棋软件,其基本结构如图21-4所示: 在图21-4中,Chessman充当原发器,ChessmanMemento充当备忘录,MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento类型的对象,用于存储备忘录。完整代码如下所示: //象棋棋子类:原发器 class Chessman { private String label; private int x; private int y; public Chessman(String label,int x,int y) { this.label = label; this.x = x; this.y = y; } public void setLabel(String label) { this.label = label; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public String getLabel() { return (this.label); } public int getX() { return (this.x); } public int getY() { return (this.y); } //保存状态 public ChessmanMemento save() { return new ChessmanMemento(this.label,this.x,this.y); } //恢复状态 public void restore(ChessmanMemento memento) { this.label = memento.getLabel(); this.x = memento.getX(); this.y = memento.getY(); } } //象棋棋子备忘录类:备忘录 class ChessmanMemento { private String label; private int x; private int y; public ChessmanMemento(String label,int x,int y) { this.label = label; this.x = x; this.y = y; } public void setLabel(String label) { this.label = label; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public String getLabel() { return (this.label); } public int getX() { return (this.x); } public int getY() { return (this.y); } } //象棋棋子备忘录管理类:负责人 class MementoCaretaker { private ChessmanMemento memento; public ChessmanMemento getMemento() { return memento; } public void setMemento(ChessmanMemento memento) { this.memento = memento; } } 编写如下客户端测试代码: class Client { public static void main(String args[]) { MementoCaretaker mc = new MementoCaretaker(); Chessman chess = new Chessman("车",1,1); display(chess); mc.setMemento(chess.save()); //保存状态 chess.setY(4); display(chess); mc.setMemento(chess.save()); //保存状态 display(chess); chess.setX(5); display(chess); System.out.println("******悔棋******"); chess.restore(mc.getMemento()); //恢复状态 display(chess); } publicstatic void display(Chessman chess) { System.out.println("棋子" + chess.getLabel() + "当前位置为:"+ "第" + chess.getX() + "行" + "第" + chess.getY() +"列。"); } } 编译并运行程序,输出结果如下: 棋子车当前位置为:第1行第1列。 棋子车当前位置为:第1行第4列。 棋子车当前位置为:第1行第4列。 棋子车当前位置为:第5行第4列。 ******悔棋****** 棋子车当前位置为:第1行第4列。 21.4 实现多次撤销 Sunny软件公司开发人员通过使用备忘录模式实现了中国象棋棋子的撤销操作,但是使用上述代码只能实现一次撤销,因为在负责人类中只定义一个备忘录对象来保存状态,后面保存的状态会将前一次保存的状态覆盖,但有时候用户需要撤销多步操作。如何实现多次撤销呢?本节将提供一种多次撤销的解决方案,那就是在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(Redo)操作,即取消撤销,让对象状态得到恢复。 改进之后的中国象棋棋子撤销功能结构图如图21-5所示: 在图21-5中,我们对负责人类MementoCaretaker进行了修改,在其中定义了一个ArrayList类型的集合对象来存储多个备忘录,其代码如下所示: import java.util.*; class MementoCaretaker { //定义一个集合来存储多个备忘录 private ArrayList mementolist = new ArrayList(); public ChessmanMemento getMemento(int i) { return (ChessmanMemento)mementolist.get(i); } public void setMemento(ChessmanMemento memento) { mementolist.add(memento); } } 编写如下客户端测试代码: class Client { privatestatic int index = -1; //定义一个索引来记录当前状态所在位置 private static MementoCaretaker mc = new MementoCaretaker(); public static void main(String args[]) { Chessman chess = new Chessman("车",1,1); play(chess); chess.setY(4); play(chess); chess.setX(5); play(chess); undo(chess,index); undo(chess,index); redo(chess,index); redo(chess,index); } //下棋 public static void play(Chessman chess) { mc.setMemento(chess.save()); //保存备忘录 index ++; System.out.println("棋子" +chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" +"第" + chess.getY() + "列。"); } //悔棋 public static void undo(Chessman chess,int i) { System.out.println("******悔棋******"); index --; chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录 System.out.println("棋子" +chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" +"第" + chess.getY() + "列。"); } //撤销悔棋 public static void redo(Chessman chess,int i) { System.out.println("******撤销悔棋******"); index ++; chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录 System.out.println("棋子" +chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" +"第" + chess.getY() + "列。"); } } 编译并运行程序,输出结果如下: 棋子车当前位置为:第1行第1列。 棋子车当前位置为:第1行第4列。 棋子车当前位置为:第5行第4列。 ******悔棋****** 棋子车当前位置为:第1行第4列。 ******悔棋****** 棋子车当前位置为:第1行第1列。 ******撤销悔棋****** 棋子车当前位置为:第1行第4列。 ******撤销悔棋****** 棋子车当前位置为:第5行第4列。 扩展 本实例只能实现最简单的Undo和Redo操作,并未考虑对象状态在操作过程中出现分支的情况。如果在撤销到某个历史状态之后,用户再修改对象状态,此后执行Undo操作时可能会发生对象状态错误,大家可以思考其产生原因。【注:可将对象状态的改变绘制成一张树状图进行分析。】 在实际开发中,可以使用链表或者堆栈来处理有分支的对象状态改变,大家可通过链表或者堆栈对上述实例进行改进。 21.5 再谈备忘录的封装 备忘录是一个很特殊的对象,只有原发器对它拥有控制的权力,负责人只负责管理,而其他类无法访问到备忘录,因此我们需要对备忘录进行封装。 为了实现对备忘录对象的封装,需要对备忘录的调用进行控制,对于原发器而言,它可以调用备忘录的所有信息,允许原发器访问返回到先前状态所需的所有数据;对于负责人而言,只负责备忘录的保存并将备忘录传递给其他对象;对于其他对象而言,只需要从负责人处取出备忘录对象并将原发器对象的状态恢复,而无须关心备忘录的保存细节。理想的情况是只允许生成该备忘录的那个原发器访问备忘录的内部状态。 在实际开发中,原发器与备忘录之间的关系是非常特殊的,它们要分享信息而不让其他类知道,实现的方法因编程语言的不同而有所差异,在C++中可以使用friend关键字,让原发器类和备忘录类成为友元类,互相之间可以访问对象的一些私有的属性;在Java语言中可以将原发器类和备忘录类放在一个包中,让它们之间满足默认的包内可见性,也可以将备忘录类作为原发器类的内部类,使得只有原发器才可以访问备忘录中的数据,其他对象都无法使用备忘录中的数据。 思考 如何使用内部类来实现备忘录模式? 21.6 备忘录模式总结 备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。 1.主要优点 备忘录模式的主要优点如下: (1)它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。 (2)备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。 2.主要缺点 备忘录模式的主要缺点如下: 资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。 3.适用场景 在以下情况下可以考虑使用备忘录模式: (1)保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。 (2)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。 练习 Sunny软件公司正在开发一款RPG网游,为了给玩家提供更多方便,在游戏过程中可以设置一个恢复点,用于保存当前的游戏场景,如果在后续游戏过程中玩家角色“不幸牺牲”,可以返回到先前保存的场景,从所设恢复点开始重新游戏。试使用备忘录模式设计该功能。 观察者模式是设计模式中的“超级模式”,其应用随处可见,在之后几篇文章里,我将向大家详细介绍观察者模式。 “红灯停,绿灯行”,在日常生活中,交通信号灯装点着我们的城市,指挥着日益拥挤的城市交通。当红灯亮起,来往的汽车将停止;而绿灯亮起,汽车可以继续前行。在这个过程中,交通信号灯是汽车(更准确地说应该是汽车驾驶员)的观察目标,而汽车是观察者。随着交通信号灯的变化,汽车的行为也将随之而变化,一盏交通信号灯可以指挥多辆汽车。如图22-1所示: 图22-1 交通信号灯与汽车示意图 【插曲:本图是根据网络上一张图PS的,但改为黑白图片之后我把那张彩色的原始图片删除了,后悔ing...,怎么根据黑白图片查询彩色图片啊,这样的工具,有木有!!那张彩色的原图很漂亮,有木有人能够帮我找一找,大哭】 在软件系统中,有些对象之间也存在类似交通信号灯和汽车之间的关系,一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变,它们之间将产生联动,正所谓“触一而牵百发”。为了更好地描述对象之间存在的这种一对多(包括一对一)的联动,观察者模式应运而生,它定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象。本章我们将学习用于实现对象间联动的观察者模式。 22.1 多人联机对战游戏的设计 Sunny软件公司欲开发一款多人联机对战游戏(类似魔兽世界、星际争霸等游戏),在该游戏中,多个玩家可以加入同一战队组成联盟,当战队中某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将作出响应。 Sunny软件公司开发人员需要提供一个设计方案来实现战队成员之间的联动。 Sunny软件公司开发人员通过对系统功能需求进行分析,发现在该系统中战队成员之间的联动过程可以简单描述如下: 联盟成员受到攻击-->发送通知给盟友-->盟友作出响应。 如果按照上述思路来设计系统,由于联盟成员在受到攻击时需要通知他的每一个盟友,因此每个联盟成员都需要持有其他所有盟友的信息,这将导致系统开销较大,因此Sunny公司开发人员决定引入一个新的角色——“战队控制中心”——来负责维护和管理每个战队所有成员的信息。当一个联盟成员受到攻击时,将向相应的战队控制中心发送求助信息,战队控制中心再逐一通知每个盟友,盟友再作出响应,如图22-2所示: 图22-2 多人联机对战游戏中对象的联动 在图22-2中,受攻击的联盟成员将与战队控制中心产生联动,战队控制中心还将与其他盟友产生联动。 如何实现对象之间的联动?如何让一个对象的状态或行为改变时,依赖于它的对象能够得到通知并进行相应的处理? 别着急,本章所介绍的观察者模式将为对象之间的联动提供一个优秀的解决方案,下面就让我们正式进入观察者模式的学习。 22.2 观察者模式概述 观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。 观察者模式定义如下: 观察者模式(Observer Pattern):定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。 观察者模式结构中通常包括观察目标和观察者两个继承层次结构,其结构如图22-3所示: 图22-3 观察者模式结构图 在观察者模式结构图中包含如下几个角色: ● Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。 ● ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。 ● Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。 ● ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。 观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。下面通过示意代码来对该模式进行进一步分析。首先我们定义一个抽象目标Subject,典型代码如下所示: import java.util.*; abstract class Subject { //定义一个观察者集合用于存储所有观察者对象 protectedArrayList observers //注册方法,用于向观察者集合中增加一个观察者 public void attach(Observer observer) { observers.add(observer); } //注销方法,用于在观察者集合中删除一个观察者 public void detach(Observer observer) { observers.remove(observer); } //声明抽象通知方法 public abstract void notify(); } 具体目标类ConcreteSubject是实现了抽象目标类Subject的一个具体子类,其典型代码如下所示: class ConcreteSubject extends Subject{ //实现通知方法 public void notify() { //遍历观察者集合,调用每一个观察者的响应方法 for(Object obs:observers) { ((Observer)obs).update(); } } } 抽象观察者角色一般定义为一个接口,通常只声明一个update()方法,为不同观察者的更新(响应)行为定义相同的接口,这个方法在其子类中实现,不同的观察者具有不同的响应方法。抽象观察者Observer典型代码如下所示: interface Observer { //声明响应方法 public void update(); } 在具体观察者ConcreteObserver中实现了update()方法,其典型代码如下所示: class ConcreteObserver implements Observer{ //实现响应方法 public void update() { //具体响应代码 } } 在有些更加复杂的情况下,具体观察者类ConcreteObserver的update()方法在执行时需要使用到具体目标类ConcreteSubject中的状态(属性),因此在ConcreteObserver与ConcreteSubject之间有时候还存在关联或依赖关系,在ConcreteObserver中定义一个ConcreteSubject实例,通过该实例获取存储在ConcreteSubject中的状态。如果ConcreteObserver的update()方法不需要使用到ConcreteSubject中的状态属性,则可以对观察者模式的标准结构进行简化,在具体观察者ConcreteObserver和具体目标ConcreteSubject之间无须维持对象引用。如果在具体层具有关联关系,系统的扩展性将受到一定的影响,增加新的具体目标类有时候需要修改原有观察者的代码,在一定程度上违反了“开闭原则”,但是如果原有观察者类无须关联新增的具体目标,则系统扩展性不受影响。 思考 观察者模式是否符合“开闭原则”?【从增加具体观察者和增加具体目标类两方面考虑。】 23.3 完整解决方案 为了实现对象之间的联动,Sunny软件公司开发人员决定使用观察者模式来进行多人联机对战游戏的设计,其基本结构如图22-4所示: 图22-4 多人联机对战游戏结构图 在图22-4中,AllyControlCenter充当目标类,ConcreteAllyControlCenter充当具体目标类,Observer充当抽象观察者,Player充当具体观察者。完整代码如下所示: import java.util.*; //抽象观察类 interface Observer { public String getName(); public void setName(String name); public void help(); //声明支援盟友方法 public void beAttacked(AllyControlCenter acc); //声明遭受攻击方法 } //战队成员类:具体观察者类 class Player implements Observer { private String name; public Player(String name) { this.name = name; } public void setName(String name) { this.name = name; } public String getName() { return this.name; } //支援盟友方法的实现 public void help() { System.out.println("坚持住," + this.name+ "来救你!"); } //遭受攻击方法的实现,当遭受攻击时将调用战队控制中心类的通知方法notifyObserver()来通知盟友 public void beAttacked(AllyControlCenter acc) { System.out.println(this.name + "被攻击!"); acc.notifyObserver(name); } } //战队控制中心类:目标类 abstract class AllyControlCenter { protected String allyName; //战队名称 protected ArrayList public void setAllyName(String allyName) { this.allyName = allyName; } public String getAllyName() { return this.allyName; } //注册方法 public void join(Observer obs) { System.out.println(obs.getName() + "加入"+ this.allyName + "战队!"); players.add(obs); } //注销方法 public void quit(Observer obs) { System.out.println(obs.getName() + "退出"+ this.allyName + "战队!"); players.remove(obs); } //声明抽象通知方法 public abstract void notifyObserver(String name); } //具体战队控制中心类:具体目标类 class ConcreteAllyControlCenter extendsAllyControlCenter { public ConcreteAllyControlCenter(String allyName) { System.out.println(allyName + "战队组建成功!"); System.out.println("----------------------------"); this.allyName = allyName; } //实现通知方法 public void notifyObserver(String name) { System.out.println(this.allyName + "战队紧急通知,盟友" + name + "遭受敌人攻击!"); //遍历观察者集合,调用每一个盟友(自己除外)的支援方法 for(Object obs : players) { if (!((Observer)obs).getName().equalsIgnoreCase(name)) { ((Observer)obs).help(); } } } } 编写如下客户端测试代码: class Client { public static void main(String args[]) { //定义观察目标对象 AllyControlCenteracc; acc = new ConcreteAllyControlCenter("金庸群侠"); //定义四个观察者对象 Observer player1,player2,player3,player4; player1 = new Player("杨过"); acc.join(player1); player2 = new Player("令狐冲"); acc.join(player2); player3 = new Player("张无忌"); acc.join(player3); player4 = new Player("段誉"); acc.join(player4); //某成员遭受攻击 player1.beAttacked(acc); } } 编译并运行程序,输出结果如下: 金庸群侠战队组建成功! ---------------------------- 杨过加入金庸群侠战队! 令狐冲加入金庸群侠战队! 张无忌加入金庸群侠战队! 段誉加入金庸群侠战队! 杨过被攻击! 金庸群侠战队紧急通知,盟友杨过遭受敌人攻击! 坚持住,令狐冲来救你! 坚持住,张无忌来救你! 坚持住,段誉来救你! 在本实例中,实现了两次对象之间的联动,当一个游戏玩家Player对象的beAttacked()方法被调用时,将调用AllyControlCenter的notifyObserver()方法来进行处理,而在notifyObserver()方法中又将调用其他Player对象的help()方法。Player的beAttacked()方法、AllyControlCenter的notifyObserver()方法以及Player的help()方法构成了一个联动触发链,执行顺序如下所示: Player.beAttacked() -->AllyControlCenter.notifyObserver() -->Player.help()。 22.4 JDK对观察者模式的支持 观察者模式在Java语言中的地位非常重要。在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持。如图22-5所示: 图22-5 JDK提供的Observable类及Observer接口结构图 (1) Observer接口 在java.util.Observer接口中只声明一个方法,它充当抽象观察者,其方法声明代码如下所示: void update(Observable o, Object arg); 当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable的notifyObservers()方法时,将执行观察者类中的update()方法。 (2) Observable类 java.util.Observable类充当观察目标类,在Observable中定义了一个向量Vector来存储观察者对象,它所包含的方法及说明见表22-1: 表22-1 Observable类所包含方法及说明 我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。 22.5 观察者模式与Java事件处理 JDK 1.0及更早版本的事件模型基于职责链模式,但是这种模型不适用于复杂的系统,因此在JDK 1.1及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(DelegationEventModel, DEM),即一个Java组件所引发的事件并不由引发事件的对象自己来负责处理,而是委派给独立的事件处理对象负责。 在DEM模型中,目标角色(如界面组件)负责发布事件,而观察者角色(事件处理者)可以向目标订阅它所感兴趣的事件。当一个具体目标产生一个事件时,它将通知所有订阅者。事件的发布者称为事件源(Event Source),而订阅者称为事件监听器(Event Listener),在这个过程中还可以通过事件对象(Event Object)来传递与事件相关的信息,可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。事件源对象、事件监听对象(事件处理对象)和事件对象构成了Java事件处理模型的三要素。事件源对象充当观察目标,而事件监听对象充当观察者。以按钮点击事件为例,其事件处理流程如下: (1) 如果用户在GUI中单击一个按钮,将触发一个事件(如ActionEvent类型的动作事件),JVM将产生一个相应的ActionEvent类型的事件对象,在该事件对象中包含了有关事件和事件源的信息,此时按钮是事件源对象; (2) 将ActionEvent事件对象传递给事件监听对象(事件处理对象),JDK提供了专门用于处理ActionEvent事件的接口ActionListener,开发人员需提供一个ActionListener的实现类(如MyActionHandler),实现在ActionListener接口中声明的抽象事件处理方法actionPerformed(),对所发生事件做出相应的处理; (3) 开发人员将ActionListener接口的实现类(如MyActionHandler)对象注册到按钮中,可以通过按钮类的addActionListener()方法来实现注册; (4) JVM在触发事件时将调用按钮的fireXXX()方法,在该方法内部将调用注册到按钮中的事件处理对象的actionPerformed()方法,实现对事件的处理。 使用类似的方法,我们可自定义GUI组件,如包含两个文本框和两个按钮的登录组件LoginBean,可以采用如图22-6所示设计方案: 图22-6 自定义登录组件结构图【省略按钮、文本框等界面组件】 图22-6中相关类说明如下: (1) LoginEvent是事件类,它用于封装与事件有关的信息,它不是观察者模式的一部分,但是它可以在目标对象和观察者对象之间传递数据,在AWT事件模型中,所有的自定义事件类都是java.util.EventObject的子类。 (2) LoginEventListener充当抽象观察者,它声明了事件响应方法validateLogin(),用于处理事件,该方法也称为事件处理方法,validateLogin()方法将一个LoginEvent类型的事件对象作为参数,用于传输与事件相关的数据,在其子类中实现该方法,实现具体的事件处理。 (3) LoginBean充当具体目标类,在这里我们没有定义抽象目标类,对观察者模式进行了一定的简化。在LoginBean中定义了抽象观察者LoginEventListener类型的对象lel和事件对象LoginEvent,提供了注册方法addLoginEventListener()用于添加观察者,在Java事件处理中,通常使用的是一对一的观察者模式,而不是一对多的观察者模式,也就是说,一个观察目标中只定义一个观察者对象,而不是提供一个观察者对象的集合。在LoginBean中还定义了通知方法fireLoginEvent(),该方法在Java事件处理模型中称为“点火方法”,在该方法内部实例化了一个事件对象LoginEvent,将用户输入的信息传给观察者对象,并且调用了观察者对象的响应方法validateLogin()。 (4) LoginValidatorA和LoginValidatorB充当具体观察者类,它们实现了在LoginEventListener接口中声明的抽象方法validateLogin(),用于具体实现事件处理,该方法包含一个LoginEvent类型的参数,在LoginValidatorA和LoginValidatorB类中可以针对相同的事件提供不同的实现。 22.6 观察者模式与MVC 在当前流行的MVC(Model-View-Controller)架构中也应用了观察者模式,MVC是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。其中模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者。当模型层的数据发生改变时,视图层将自动改变其显示内容。如图22-7所示: 图22-7 MVC结构示意图 在图22-7中,模型层提供的数据是视图层所观察的对象,在视图层中包含两个用于显示数据的图表对象,一个是柱状图,一个是饼状图,相同的数据拥有不同的图表显示方式,如果模型层的数据发生改变,两个图表对象将随之发生变化,这意味着图表对象依赖模型层提供的数据对象,因此数据对象的任何状态改变都应立即通知它们。同时,这两个图表之间相互独立,不存在任何联系,而且图表对象的个数没有任何限制,用户可以根据需要再增加新的图表对象,如折线图。在增加新的图表对象时,无须修改原有类库,满足“开闭原则”。 扩展 大家可以查阅相关资料对MVC模式进行深入学习,如Oracle公司提供的技术文档《Java SE Application DesignWith MVC》,参考链接:http://www.oracle.com/technetwork/articles/javase/index-142890.html。 22.7 观察者模式总结 观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用或者桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的GUI事件处理的实现,在基于事件的XML解析技术(如SAX2)以及Web事件处理中也都使用了观察者模式。 1.主要优点 观察者模式的主要优点如下: (1) 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。 (2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。 (3) 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。 (4) 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。 2.主要缺点 观察者模式的主要缺点如下: (1) 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。 (2) 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 (3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。 3.适用场景 在以下情况下可以考虑使用观察者模式: (1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。 (2) 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。 (3) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。 练习 Sunny软件公司欲开发一款实时在线股票软件,该软件需提供如下功能:当股票购买者所购买的某支股票价格变化幅度达到5%时,系统将自动发送通知(包括新价格)给购买该股票的所有股民。试使用观察者模式设计并实现该系统。 “人有悲欢离合,月有阴晴圆缺”,包括人在内,很多事物都具有多种状态,而且在不同状态下会具有不同的行为,这些状态在特定条件下还将发生相互转换。就像水,它可以凝固成冰,也可以受热蒸发后变成水蒸汽,水可以流动,冰可以雕刻,蒸汽可以扩散。我们可以用UML状态图来描述H2O的三种状态,如图1所示: 图1 H2O的三种状态(未考虑临界点) 在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计,我们可以使用一种被称之为状态模式的设计模式,本章我们将学习用于描述对象状态及其转换的状态模式。 银行系统中的账户类设计 Sunny软件公司欲为某银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,Sunny软件公司开发人员发现在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下: (1) 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款; (2) 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息; (3) 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息; (4) 根据余额的不同,以上三种状态可发生相互转换。 Sunny软件公司开发人员对银行账户类进行分析,绘制了如图2所示UML状态图: 图2 银行账户状态图 在图2中,NormalState表示正常状态,OverdraftState表示透支状态,RestrictedState表示受限状态,在这三种状态下账户对象拥有不同的行为,方法deposit()用于存款,withdraw()用于取款,computeInterest()用于计算利息,stateCheck()用于在每一次执行存款和取款操作后根据余额来判断是否要进行状态转换并实现状态转换,相同的方法在不同的状态中可能会有不同的实现。为了实现不同状态下对象的各种行为以及对象状态之间的相互转换,Sunny软件公司开发人员设计了一个较为庞大的账户类Account,其中部分代码如下所示: class Account { private String state; //状态 private int balance; //余额 ...... //存款操作 public void deposit() { //存款 stateCheck(); } //取款操作 public void withdraw() { if (state.equalsIgnoreCase("NormalState") ||state.equalsIgnoreCase("OverdraftState ")) { //取款 stateCheck(); } else { //取款受限 } } //计算利息操作 public void computeInterest() { if(state.equalsIgnoreCase("OverdraftState") ||state.equalsIgnoreCase("RestrictedState ")) { //计算利息 } } //状态检查和转换操作 public void stateCheck() { if (balance >= 0) { state = "NormalState"; } else if (balance > -2000 && balance < 0) { state = "OverdraftState"; } else if (balance == -2000) { state = "RestrictedState"; } else if (balance < -2000) { //操作受限 } } ...... } 分析上述代码,我们不难发现存在如下几个问题: (1) 几乎每个方法中都包含状态判断语句,以判断在该状态下是否具有该方法以及在特定状态下该方法如何实现,导致代码非常冗长,可维护性较差; (2) 拥有一个较为复杂的stateCheck()方法,包含大量的if…else if…else…语句用于进行状态转换,代码测试难度较大,且不易于维护; (3) 系统扩展性较差,如果需要增加一种新的状态,如冻结状态(Frozen State,在该状态下既不允许存款也不允许取款),需要对原有代码进行大量修改,扩展起来非常麻烦。 为了解决这些问题,我们可以使用状态模式,在状态模式中,我们将对象在每一个状态下的行为和状态转移语句封装在一个个状态类中,通过这些状态类来分散冗长的条件转移语句,让系统具有更好的灵活性和可扩展性,状态模式可以在一定程度上解决上述问题。 2 状态模式概述 状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。 状态模式定义如下: 状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。 在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构如图3所示: 图3 状态模式结构图 在状态模式结构图中包含如下几个角色: ● Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。 ● State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。 ● ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。 在状态模式中,我们将对象在不同状态下的行为封装到不同的状态类中,为了让系统具有更好的灵活性和可扩展性,同时对各状态下的共有行为进行封装,我们需要对状态进行抽象,引入了抽象状态类角色,其典型代码如下所示: abstract class State { //声明抽象业务方法,不同的具体状态类可以不同的实现 public abstract void handle(); } 在抽象状态类的子类即具体状态类中实现了在抽象状态类中声明的业务方法,不同的具体状态类可以提供完全不同的方法实现,在实际使用时,在一个状态类中可能包含多个业务方法,如果在具体状态类中某些业务方法的实现完全相同,可以将这些方法移至抽象状态类,实现代码的复用,典型的具体状态类代码如下所示: class ConcreteState extends State { public void handle() { //方法具体实现代码 } } 环境类维持一个对抽象状态类的引用,通过setState()方法可以向环境类注入不同的状态对象,再在环境类的业务方法中调用状态对象的方法,典型代码如下所示: class Context { private State state; //维持一个对抽象状态对象的引用 private int value; //其他属性值,该属性值的变化可能会导致对象状态发生变化 //设置状态对象 public void setState(State state) { this.state = state; } public void request() { //其他代码 state.handle(); //调用状态对象的业务方法 //其他代码 } } 环境类实际上是真正拥有状态的对象,我们只是将环境类中与状态有关的代码提取出来封装到专门的状态类中。在状态模式结构图中,环境类Context与抽象状态类State之间存在单向关联关系,在Context中定义了一个State对象。在实际使用时,它们之间可能存在更为复杂的关系,State与Context之间可能也存在依赖或者关联关系。 在状态模式的使用过程中,一个对象的状态之间还可以进行相互转换,通常有两种实现状态转换的方式: (1) 统一由环境类来负责状态之间的转换,此时,环境类还充当了状态管理器(State Manager)角色,在环境类的业务方法中通过对某些属性值的判断实现状态转换,还可以提供一个专门的方法用于实现属性判断和状态转换,如下代码片段所示: …… public void changeState() { //判断属性值,根据属性值进行状态转换 if (value == 0) { this.setState(new ConcreteStateA()); } else if (value == 1) { this.setState(new ConcreteStateB()); } ...... } …… (2) 由具体状态类来负责状态之间的转换,可以在具体状态类的业务方法中判断环境类的某些属性值再根据情况为环境类设置新的状态对象,实现状态转换,同样,也可以提供一个专门的方法来负责属性值的判断和状态转换。此时,状态类与环境类之间就将存在依赖或关联关系,因为状态类需要访问环境类中的属性值,如下代码片段所示: …… public void changeState(Context ctx) { //根据环境对象中的属性值进行状态转换 if (ctx.getValue() == 1) { ctx.setState(new ConcreteStateB()); } else if (ctx.getValue() == 2) { ctx.setState(new ConcreteStateC()); } ...... } …… 思考 理解两种状态转换方式的异同? 3 完整解决方案 Sunny软件公司开发人员使用状态模式来解决账户状态的转换问题,客户端只需要执行简单的存款和取款操作,系统根据余额将自动转换到相应的状态,其基本结构如图4所示: 图4 银行账户结构图 在图4中,Account充当环境类角色,AccountState充当抽象状态角色,NormalState、OverdraftState和RestrictedState充当具体状态角色。完整代码如下所示:温馨提示:代码有点长,需要有耐心! //银行账户:环境类 class Account { private AccountState state; //维持一个对抽象状态对象的引用 private String owner; //开户名 private double balance = 0; //账户余额 public Account(String owner,double init) { this.owner = owner; this.balance = balance; this.state = new NormalState(this); //设置初始状态 System.out.println(this.owner + "开户,初始金额为"+ init); System.out.println("---------------------------------------------"); } public double getBalance() { return this.balance; } public void setBalance(double balance) { this.balance = balance; } public void setState(AccountState state) { this.state = state; } public void deposit(double amount) { System.out.println(this.owner + "存款" +amount); state.deposit(amount); //调用状态对象的deposit()方法 System.out.println("现在余额为"+this.balance); System.out.println("现在帐户状态为"+this.state.getClass().getName()); System.out.println("---------------------------------------------"); } public void withdraw(double amount) { System.out.println(this.owner + "取款" +amount); state.withdraw(amount); //调用状态对象的withdraw()方法 System.out.println("现在余额为"+this.balance); System.out.println("现在帐户状态为"+ this.state.getClass().getName()); System.out.println("---------------------------------------------"); } public void computeInterest() { state.computeInterest(); //调用状态对象的computeInterest()方法 } } //抽象状态类 abstract class AccountState { protected Account acc; public abstract void deposit(double amount); public abstract void withdraw(double amount); public abstract void computeInterest(); public abstract void stateCheck(); } //正常状态:具体状态类 class NormalState extends AccountState{ public NormalState(Account acc) { this.acc = acc; } publicNormalState(AccountState state) { this.acc = state.acc; } public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } public void withdraw(double amount) { acc.setBalance(acc.getBalance() - amount); stateCheck(); } public void computeInterest() { System.out.println("正常状态,无须支付利息!"); } //状态转换 public void stateCheck() { if (acc.getBalance() > -2000 && acc.getBalance() <= 0){ acc.setState(new OverdraftState(this)); } else if (acc.getBalance() == -2000) { acc.setState(new RestrictedState(this)); } else if (acc.getBalance() < -2000) { System.out.println("操作受限!"); } } } //透支状态:具体状态类 class OverdraftState extendsAccountState { public OverdraftState(AccountState state) { this.acc = state.acc; } public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } public void withdraw(double amount) { acc.setBalance(acc.getBalance() - amount); stateCheck(); } public void computeInterest() { System.out.println("计算利息!"); } //状态转换 public void stateCheck() { if (acc.getBalance() > 0) { acc.setState(new NormalState(this)); } else if (acc.getBalance() == -2000) { acc.setState(new RestrictedState(this)); } else if (acc.getBalance() < -2000) { System.out.println("操作受限!"); } } } //受限状态:具体状态类 class RestrictedState extends AccountState{ public RestrictedState(AccountState state) { this.acc = state.acc; } public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } public void withdraw(double amount) { System.out.println("帐号受限,取款失败"); } public void computeInterest() { System.out.println("计算利息!"); } //状态转换 public void stateCheck() { if(acc.getBalance() > 0) { acc.setState(new NormalState(this)); } else if(acc.getBalance() > -2000) { acc.setState(new OverdraftState(this)); } } } 编写如下客户端测试代码: class Client { public static void main(String args[]) { Account acc = new Account("段誉",0.0); acc.deposit(1000); acc.withdraw(2000); acc.deposit(3000); acc.withdraw(4000); acc.withdraw(1000); acc.computeInterest(); } } 编译并运行程序,输出结果如下: 段誉开户,初始金额为0.0 --------------------------------------------- 段誉存款1000.0 现在余额为1000.0 现在帐户状态为NormalState --------------------------------------------- 段誉取款2000.0 现在余额为-1000.0 现在帐户状态为OverdraftState --------------------------------------------- 段誉存款3000.0 现在余额为2000.0 现在帐户状态为NormalState --------------------------------------------- 段誉取款4000.0 现在余额为-2000.0 现在帐户状态为RestrictedState --------------------------------------------- 段誉取款1000.0 帐号受限,取款失败 现在余额为-2000.0 现在帐户状态为RestrictedState --------------------------------------------- 计算利息! 4 共享状态 在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。 下面通过一个简单实例来说明如何实现共享状态: 如果某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。 可以使用状态模式来实现开关的设计,其结构如图5所示: 图5 开关及其状态设计结构图 开关类Switch代码如下所示: class Switch { private static State state,onState,offState; //定义三个静态的状态对象 private String name; public Switch(String name) { this.name = name; onState = new OnState(); offState = new OffState(); this.state = onState; } public void setState(State state) { this.state = state; } public static State getState(String type) { if (type.equalsIgnoreCase("on")) { return onState; } else { return offState; } } //打开开关 public void on() { System.out.print(name); state.on(this); } //关闭开关 public void off() { System.out.print(name); state.off(this); } } 抽象状态类如下代码所示: abstract class State { public abstract void on(Switch s); public abstract void off(Switch s); } 两个具体状态类如下代码所示: //打开状态 class OnState extends State { public void on(Switch s) { System.out.println("已经打开!"); } public void off(Switch s) { System.out.println("关闭!"); s.setState(Switch.getState("off")); } } //关闭状态 class OffState extends State { public void on(Switch s) { System.out.println("打开!"); s.setState(Switch.getState("on")); } public void off(Switch s) { System.out.println("已经关闭!"); } } 编写如下客户端代码进行测试: class Client { public static void main(String args[]) { Switch s1,s2; s1=new Switch("开关1"); s2=new Switch("开关2"); s1.on(); s2.on(); s1.off(); s2.off(); s2.on(); s1.on(); } } 输出结果如下: 开关1已经打开! 开关2已经打开! 开关1关闭! 开关2已经关闭! 开关2打开! 开关1已经打开! 从输出结果可以得知两个开关共享相同的状态,如果第一个开关关闭,则第二个开关也将关闭,再次关闭时将输出“已经关闭”;打开时也将得到类似结果。 5 使用环境类实现状态转换 在状态模式中实现状态转换时,具体状态类可通过调用环境类Context的setState()方法进行状态的转换操作,也可以统一由环境类Context来实现状态的转换。此时,增加新的具体状态类可能需要修改其他具体状态类或者环境类的源代码,否则系统无法转换到新增状态。但是对于客户端来说,无须关心状态类,可以为环境类设置默认的状态类,而将状态的转换工作交给具体状态类或环境类来完成,具体的转换细节对于客户端而言是透明的。 在上面的“银行账户状态转换”实例中,我们通过具体状态类来实现状态的转换,在每一个具体状态类中都包含一个stateCheck()方法,在该方法内部实现状态的转换,除此之外,我们还可以通过环境类来实现状态转换,环境类作为一个状态管理器,统一实现各种状态之间的转换操作。 下面通过一个包含循环状态的简单实例来说明如何使用环境类实现状态转换: Sunny软件公司某开发人员欲开发一个屏幕放大镜工具,其具体功能描述如下: 用户单击“放大镜”按钮之后屏幕将放大一倍,再点击一次“放大镜”按钮屏幕再放大一倍,第三次点击该按钮后屏幕将还原到默认大小。 可以考虑使用状态模式来设计该屏幕放大镜工具,我们定义三个屏幕状态类NormalState、LargerState和LargestState来对应屏幕的三种状态,分别是正常状态、二倍放大状态和四倍放大状态,屏幕类Screen充当环境类,其结构如图6所示: 图6 屏幕放大镜工具结构图 本实例核心代码如下所示: //屏幕类 class Screen { //枚举所有的状态,currentState表示当前状态 private State currentState, normalState, largerState, largestState; public Screen() { this.normalState = new NormalState(); //创建正常状态对象 this.largerState = new LargerState(); //创建二倍放大状态对象 this.largestState = new LargestState(); //创建四倍放大状态对象 this.currentState = normalState; //设置初始状态 this.currentState.display(); } public void setState(State state) { this.currentState = state; } //单击事件处理方法,封转了对状态类中业务方法的调用和状态的转换 public void onClick() { if (this.currentState == normalState) { this.setState(largerState); this.currentState.display(); } else if (this.currentState == largerState) { this.setState(largestState); this.currentState.display(); } else if (this.currentState == largestState) { this.setState(normalState); this.currentState.display(); } } } //抽象状态类 abstract class State { public abstract void display(); } //正常状态类 class NormalState extends State{ public void display() { System.out.println("正常大小!"); } } //二倍状态类 class LargerState extends State{ public void display() { System.out.println("二倍大小!"); } } //四倍状态类 class LargestState extends State{ public void display() { System.out.println("四倍大小!"); } } 在上述代码中,所有的状态转换操作都由环境类Screen来实现,此时,环境类充当了状态管理器角色。如果需要增加新的状态,例如“八倍状态类”,需要修改环境类,这在一定程度上违背了“开闭原则”,但对其他状态类没有任何影响。 编写如下客户端代码进行测试: class Client { public static void main(String args[]) { Screen screen = new Screen(); screen.onClick(); screen.onClick(); screen.onClick(); } } 输出结果如下: 正常大小! 二倍大小! 四倍大小! 正常大小! 6 状态模式总结 状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。 主要优点 状态模式的主要优点如下: (1) 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。 (2) 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。 (3) 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。 (4) 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。 主要缺点 状态模式的主要缺点如下: (1) 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。 (2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。 (3) 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。 适用场景 在以下情况下可以考虑使用状态模式: (1) 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。 (2) 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。 练习 Sunny软件公司欲开发一款纸牌游戏软件,在该游戏软件中用户角色具有入门级(Primary)、熟练级(Secondary)、高手级(Professional)和骨灰级(Final)四种等级,角色的等级与其积分相对应,游戏胜利将增加积分,失败则扣除积分。入门级具有最基本的游戏功能play() ,熟练级增加了游戏胜利积分加倍功能doubleScore(),高手级在熟练级基础上再增加换牌功能changeCards(),骨灰级在高手级基础上再增加偷看他人的牌功能peekCards()。 试使用状态模式来设计该系统。 俗话说:条条大路通罗马。在很多情况下,实现某个目标的途径不止一条,例如我们在外出旅游时可以选择多种不同的出行方式,如骑自行车、坐汽车、坐火车或者坐飞机,可根据实际情况(目的地、旅游预算、旅游时间等)来选择一种最适合的出行方式。在制订旅行计划时,如果目的地较远、时间不多,但不差钱,可以选择坐飞机去旅游;如果目的地虽远、但假期长、且需控制旅游成本时可以选择坐火车或汽车;如果从健康和环保的角度考虑,而且有足够的毅力,自行车游或者徒步旅游也是个不错的选择,大笑。 在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。本章我们将介绍一种为了适应算法灵活性而产生的设计模式——策略模式。 24.1 电影票打折方案 Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下: (1) 学生凭学生证可享受票价8折优惠; (2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元); (3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。 该系统在将来可能还要根据需要引入新的打折方式。 为了实现上述电影票打折功能,Sunny软件公司开发人员设计了一个电影票类MovieTicket,其核心代码片段如下所示: //电影票类 class MovieTicket { private double price; //电影票价格 private String type; //电影票类型 public void setPrice(double price) { this.price = price; } public void setType(String type) { this.type = type; } public double getPrice() { return this.calculate(); } //计算打折之后的票价 public double calculate() { //学生票折后票价计算 if(this.type.equalsIgnoreCase("student")) { System.out.println("学生票:"); return this.price * 0.8; } //儿童票折后票价计算 else if(this.type.equalsIgnoreCase("children") &&this.price >= 20 ) { System.out.println("儿童票:"); return this.price - 10; } //VIP票折后票价计算 else if(this.type.equalsIgnoreCase("vip")) { System.out.println("VIP票:"); System.out.println("增加积分!"); return this.price * 0.5; } else { return this.price; //如果不满足任何打折要求,则返回原始票价 } } } 编写如下客户端测试代码: class Client { public static void main(String args[]) { MovieTicket mt = new MovieTicket(); double originalPrice = 60.0; //原始票价 double currentPrice; //折后价 mt.setPrice(originalPrice); System.out.println("原始价为:" +originalPrice); System.out.println("---------------------------------"); mt.setType("student"); //学生票 currentPrice = mt.getPrice(); System.out.println("折后价为:" +currentPrice); System.out.println("---------------------------------"); mt.setType("children"); //儿童票 currentPrice = mt.getPrice(); System.out.println("折后价为:" +currentPrice); } } 编译并运行程序,输出结果如下所示: 原始价为:60.0 --------------------------------- 学生票: 折后价为:48.0 --------------------------------- 儿童票: 折后价为:50.0 通过MovieTicket类实现了电影票的折后价计算,该方案解决了电影票打折问题,每一种打折方式都可以称为一种打折算法,更换打折方式只需修改客户端代码中的参数,无须修改已有源代码,但该方案并不是一个完美的解决方案,它至少存在如下三个问题: (1) MovieTicket类的calculate()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护。 (2) 增加新的打折算法或者对原有打折算法进行修改时必须修改MovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。 (3) 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。 如何解决这三个问题?导致产生这些问题的主要原因在于MovieTicket类职责过重,它将各种打折算法都定义在一个类中,这既不便于算法的重用,也不便于算法的扩展。因此我们需要对MovieTicket类进行重构,将原本庞大的MovieTicket类的职责进行分解,将算法的定义和使用分离,这就是策略模式所要解决的问题,下面将进入策略模式的学习。 24.2 策略模式概述 在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。 策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。策略模式定义如下:策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。 策略模式结构并不复杂,但我们需要理解其中环境类Context的作用,其结构如图24-1所示: 在策略模式结构图中包含如下几个角色: ● Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。 ● Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。 ● ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。 思考 一个环境类Context能否对应多个不同的策略等级结构?如何设计? 策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。在策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。 在使用策略模式时,我们需要将算法从Context类中提取出来,首先应该创建一个抽象策略类,其典型代码如下所示: abstract class AbstractStrategy { public abstract void algorithm(); //声明抽象算法 } 然后再将封装每一种具体算法的类作为该抽象策略类的子类,如下代码所示: class ConcreteStrategyA extendsAbstractStrategy { //算法的具体实现 public void algorithm() { //算法A } } 其他具体策略类与之类似,对于Context类而言,在它与抽象策略类之间建立一个关联关系,其典型代码如下所示: class Context { privateAbstractStrategy strategy; //维持一个对抽象策略类的引用 public void setStrategy(AbstractStrategy strategy) { this.strategy= strategy; } //调用策略类中的算法 public void algorithm() { strategy.algorithm(); } } 在Context类中定义一个AbstractStrategy类型的对象strategy,通过注入的方式在客户端传入一个具体策略对象,客户端代码片段如下所示: …… Context context = new Context(); AbstractStrategy strategy; strategy = new ConcreteStrategyA(); //可在运行时指定类型 context.setStrategy(strategy); context.algorithm(); …… 在客户端代码中只需注入一个具体策略对象,可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。 24.3 完整解决方案 为了实现打折算法的复用,并能够灵活地向系统中增加新的打折方式,Sunny软件公司开发人员使用策略模式对电影院打折方案进行重构,重构后基本结构如图24-2所示: 在图24-2中,MovieTicket充当环境类角色,Discount充当抽象策略角色,StudentDiscount、 ChildrenDiscount 和VIPDiscount充当具体策略角色。完整代码如下所示: //电影票类:环境类 class MovieTicket { private double price; private Discount discount; //维持一个对抽象折扣类的引用 public void setPrice(double price) { this.price = price; } //注入一个折扣类对象 public void setDiscount(Discount discount) { this.discount = discount; } public double getPrice() { //调用折扣类的折扣价计算方法 return discount.calculate(this.price); } } //折扣类:抽象策略类 interface Discount { public double calculate(double price); } //学生票折扣类:具体策略类 class StudentDiscount implements Discount{ public double calculate(double price) { System.out.println("学生票:"); return price * 0.8; } } //儿童票折扣类:具体策略类 class ChildrenDiscount implements Discount{ public double calculate(double price) { System.out.println("儿童票:"); return price - 10; } } //VIP会员票折扣类:具体策略类 class VIPDiscount implements Discount{ public double calculate(double price) { System.out.println("VIP票:"); System.out.println("增加积分!"); return price * 0.5; } } 为了提高系统的灵活性和可扩展性,我们将具体策略类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示: import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.SAXException; import java.io.*; class XMLUtil { //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象 public static Object getBean() { try { //创建文档对象 DocumentBuilderFactory dFactory =DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File("config.xml")); //获取包含类名的文本节点 NodeList nl = doc.getElementsByTagName("className"); Node classNode=nl.item(0).getFirstChild(); String cName=classNode.getNodeValue(); //通过类名生成实例对象并将其返回 Class c=Class.forName(cName); Object obj=c.newInstance(); return obj; } catch(Exception e) { e.printStackTrace(); return null; } } } 在配置文件config.xml中存储了具体策略类的类名,代码如下所示: 编写如下客户端测试代码: class Client { public static void main(String args[]) { MovieTicket mt = new MovieTicket(); double originalPrice = 60.0; double currentPrice; mt.setPrice(originalPrice); System.out.println("原始价为:" +originalPrice); System.out.println("---------------------------------"); Discount discount; discount = (Discount)XMLUtil.getBean(); //读取配置文件并反射生成具体折扣对象 mt.setDiscount(discount); //注入折扣对象 currentPrice = mt.getPrice(); System.out.println("折后价为:" +currentPrice); } } 编译并运行程序,输出结果如下: 原始价为:60.0 --------------------------------- 学生票: 折后价为:48.0 如果需要更换具体策略类,无须修改源代码,只需修改配置文件,例如将学生票改为儿童票,只需将存储在配置文件中的具体策略类StudentDiscount改为ChildrenDiscount,如下代码所示: 重新运行客户端程序,输出结果如下: 原始价为:60.0 --------------------------------- 儿童票: 折后价为:50.0 如果需要增加新的打折方式,原有代码均无须修改,只要增加一个新的折扣类作为抽象折扣类的子类,实现在抽象折扣类中声明的打折方法,然后修改配置文件,将原有具体折扣类类名改为新增折扣类类名即可,完全符合“开闭原则”。 24.4 策略模式的两个典型应用 策略模式实用性强、扩展性好,在软件开发中得以广泛使用,是使用频率较高的设计模式之一。下面将介绍策略模式的两个典型应用实例,一个来源于Java SE,一个来源于微软公司推出的演示项目PetShop。 (1) Java SE的容器布局管理就是策略模式的一个经典应用实例,其基本结构示意图如图24-3所示: 【每次看到这个LayoutManager2接口,我都在想当时Sun公司开发人员是怎么想的!微笑】 在Java SE开发中,用户需要对容器对象Container中的成员对象如按钮、文本框等GUI控件进行布局(Layout),在程序运行期间由客户端动态决定一个Container对象如何布局,Java语言在JDK中提供了几种不同的布局方式,封装在不同的类中,如BorderLayout、FlowLayout、GridLayout、GridBagLayout和CardLayout等。在图24-3中,Container类充当环境角色Context,而LayoutManager作为所有布局类的公共父类扮演了抽象策略角色,它给出所有具体布局类所需的接口,而具体策略类是LayoutManager的子类,也就是各种具体的布局类,它们封装了不同的布局方式。 任何人都可以设计并实现自己的布局类,只需要将自己设计的布局类作为LayoutManager的子类就可以,比如传奇的Borland公司(现在已是传说)曾在JBuilder中提供了一种新的布局方式——XYLayout,作为对JDK提供的Layout类的补充。对于客户端而言,只需要使用Container类提供的setLayout()方法就可设置任何具体布局方式,无须关心该布局的具体实现。在JDK中,Container类的代码片段如下: public class Container extends Component{ …… LayoutManager layoutMgr; …… public void setLayout(LayoutManager mgr) { layoutMgr = mgr; …… } …… } 从上述代码可以看出,Container作为环境类,针对抽象策略类LayoutManager进行编程,用户在使用时,根据“里氏代换原则”,只需要在setLayout()方法中传入一个具体布局对象即可,无须关心它的具体实现。 (2) 除了基于Java语言的应用外,在使用其他面向对象技术开发的软件中,策略模式也得到了广泛的应用。 在微软公司提供的演示项目PetShop 4.0中就使用策略模式来处理同步订单和异步订单的问题。在PetShop 4.0的BLL(BusinessLogic Layer,业务逻辑层)子项目中有一个OrderAsynchronous类和一个OrderSynchronous类,它们都继承自IOrderStrategy接口,如图24-4所示: 在图24-4中,OrderSynchronous以一种同步的方式处理订单,而OrderAsynchronous先将订单存放在一个队列中,然后再对队列里的订单进行处理,以一种异步方式对订单进行处理。BLL的Order类通过反射机制从配置文件中读取策略配置的信息,以决定到底是使用哪种订单处理方式。配置文件web.config中代码片段如下所示: …… …… 用户只需要修改配置文件即可更改订单处理方式,提高了系统的灵活性。 24.5 策略模式总结 策略模式用于算法的自由切换和扩展,它是应用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族,允许用户从该算法族中任选一个算法来解决某一问题,同时可以方便地更换算法或者增加新的算法。只要涉及到算法的封装、复用和切换都可以考虑使用策略模式。 主要优点 策略模式的主要优点如下: (1) 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。 (2) 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。 (3) 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。 (4) 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。 (5) 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。 主要缺点 策略模式的主要缺点如下: (1) 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。 (2) 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。 (3) 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。 适用场景 在以下情况下可以考虑使用策略模式: (1) 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据“里氏代换原则”和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象。 (2) 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。 (3) 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。 模板方法模式概述 在现实生活中,很多事情都包含几个实现步骤,例如请客吃饭,无论吃什么,一般都包含点单、吃东西、买单等几个步骤,通常情况下这几个步骤的次序是:点单 --> 吃东西 --> 买单。在这三个步骤中,点单和买单大同小异,最大的区别在于第二步——吃什么?吃面条和吃满汉全席可大不相同,如图1所示: 图1 请客吃饭示意图 在软件开发中,有时也会遇到类似的情况,某个方法的实现需要多个步骤(类似“请客”),其中有些步骤是固定的(类似“点单”和“买单”),而有些步骤并不固定,存在可变性(类似“吃东西”)。为了提高代码的复用性和系统的灵活性,可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计,在模板方法模式中,将实现功能的每一个步骤所对应的方法称为基本方法(例如“点单”、“吃东西”和“买单”),而调用这些基本方法同时定义基本方法的执行次序的方法称为模板方法(例如“请客”)。在模板方法模式中,可以将相同的代码放在父类中,例如将模板方法“请客”以及基本方法“点单”和“买单”的实现放在父类中,而对于基本方法“吃东西”,在父类中只做一个声明,将其具体实现放在不同的子类中,在一个子类中提供“吃面条”的实现,而另一个子类提供“吃满汉全席”的实现。通过使用模板方法模式,一方面提高了代码的复用性,另一方面还可以利用面向对象的多态性,在运行时选择一种具体子类,实现完整的“请客”方法,提高系统的灵活性和可扩展性。 模板方法模式定义如下: 模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 Template Method Pattern: Define theskeleton of an algorithm in an operation, deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm withoutchanging the algorithm's structure. 模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。 模板方法模式是结构最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。模板方法模式提供了一个模板方法来定义算法框架,而某些具体步骤的实现可以在其子类中完成。 模板方法模式结构与实现 2.1 模式结构 模板方法模式结构比较简单,其核心是抽象类和其中的模板方法的设计,其结构如图2所示: 图2 模板方法模式结构图 由图2可知,模板方法模式包含如下两个角色: (1) AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。 (2) ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。 2.2 模式实现 在实现模板方法模式时,开发抽象类的软件设计师和开发具体子类的软件设计师之间可以进行协作。一个设计师负责给出一个算法的轮廓和框架,另一些设计师则负责给出这个算法的各个逻辑步骤。实现这些具体逻辑步骤的方法即为基本方法,而将这些基本方法汇总起来的方法即为模板方法,模板方法模式的名字也因此而来。下面将详细介绍模板方法和基本方法: 模板方法 一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模板方法定义在抽象类中,并由子类不加以修改地完全继承下来。模板方法是一个具体方法,它给出了一个顶层逻辑框架,而逻辑的组成步骤在抽象类中可以是具体方法,也可以是抽象方法。由于模板方法是具体方法,因此模板方法模式中的抽象层只能是抽象类,而不是接口。 基本方法 基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。 (1) 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。在C#语言里一个抽象方法以abstract关键字标识。 (2) 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。 (3) 钩子方法:一个钩子方法由一个抽象类或具体类声明并实现,而其子类可能会加以扩展。通常在父类中给出的实现是一个空实现(可使用virtual关键字将其定义为虚函数),并以该空实现作为方法的默认实现,当然钩子方法也可以提供一个非空的默认实现。 在模板方法模式中,钩子方法有两类:第一类钩子方法可以与一些具体步骤“挂钩”,以实现在不同条件下执行模板方法中的不同步骤,这类钩子方法的返回类型通常是bool类型的,这类方法名一般为IsXXX(),用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行,如下代码片段所示: …… //模板方法 public void TemplateMethod() { Open(); Display(); //通过钩子方法来确定某步骤是否执行 if(IsPrint()) { Print(); } } //钩子方法 public bool IsPrint() { return true; } …… 在代码中IsPrint()方法即是钩子方法,它可以决定Print()方法是否执行,一般情况下,钩子方法的返回值为true,如果不希望某方法执行,可以在其子类中覆盖钩子方法,将其返回值改为false即可,这种类型的钩子方法可以控制方法的执行,对一个算法进行约束。 还有一类钩子方法就是实现体为空的具体方法,子类可以根据需要覆盖或者继承这些钩子方法,与抽象方法相比,这类钩子方法的好处在于子类如果没有覆盖父类中定义的钩子方法,编译可以正常通过,但是如果没有覆盖父类中声明的抽象方法,编译将报错。 在模板方法模式中,抽象类的典型代码如下: abstract class AbstractClass { //模板方法 publicvoid TemplateMethod() { PrimitiveOperation1(); PrimitiveOperation2(); PrimitiveOperation3(); } //基本方法—具体方法 publicvoid PrimitiveOperation1() { //实现代码 } //基本方法—抽象方法 public abstract void PrimitiveOperation2(); //基本方法—钩子方法 publicvirtual void PrimitiveOperation3() { } } 在抽象类中,模板方法TemplateMethod()定义了算法的框架,在模板方法中调用基本方法以实现完整的算法,每一个基本方法如PrimitiveOperation1()、PrimitiveOperation2()等均实现了算法的一部分,对于所有子类都相同的基本方法可在父类提供具体实现,例如PrimitiveOperation1(),否则在父类声明为抽象方法或钩子方法,由不同的子类提供不同的实现,例如PrimitiveOperation2()和PrimitiveOperation3()。 可在抽象类的子类中提供抽象步骤的实现,也可覆盖父类中已经实现的具体方法,具体子类的典型代码如下: class ConcreteClass : AbstractClass { publicoverride void PrimitiveOperation2() { //实现代码 } publicoverride void PrimitiveOperation3() { //实现代码 } } 在模板方法模式中,由于面向对象的多态性,子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法,因此程序在运行时,具体子类的基本方法将覆盖父类中定义的基本方法,子类的钩子方法也将覆盖父类的钩子方法,从而可以通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制。 3 模板方法模式应用实例 下面通过一个应用实例来进一步学习和理解模板方法模式。 实例说明 某软件公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下: (1) 系统根据账号和密码验证用户信息,如果用户信息错误,系统显示出错提示; (2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式); (3) 系统显示利息。 试使用模板方法模式设计该利息计算模块。 实例类图 通过分析,本实例结构图如图3所示。 图3 银行利息计算模块结构图 在图3中,Account充当抽象类角色,CurrentAccount和SavingAccount充当具体子类角色。 实例代码 (1) Account:账户类,充当抽象类。 //Account.cs using System; namespace TemplateMethodSample { abstract class Account { //基本方法——具体方法 public bool Validate(string account, string password) { Console.WriteLine("账号:{0}",account); Console.WriteLine("密码:{0}",password); //模拟登录 if (account.Equals("张无忌") &&password.Equals("123456")) { return true; } else { return false; } } //基本方法——抽象方法 public abstract void CalculateInterest(); //基本方法——具体方法 public void Display() { Console.WriteLine("显示利息!"); } //模板方法 public void Handle(string account, string password) { if (!Validate(account,password)) { Console.WriteLine("账户或密码错误!"); return; } CalculateInterest(); Display(); } } } (2) CurrentAccount:活期账户类,充当具体子类。 //CurrentAccount.cs using System; namespace TemplateMethodSample { class CurrentAccount : Account { //覆盖父类的抽象基本方法 public override void CalculateInterest() { Console.WriteLine("按活期利率计算利息!"); } } } (3) SavingAccount:定期账户类,充当具体子类。 //SavingAccount.cs using System; namespace TemplateMethodSample { class SavingAccount : Account { //覆盖父类的抽象基本方法 public override void CalculateInterest() { Console.WriteLine("按定期利率计算利息!"); } } } (4) 配置文件App.config,在配置文件中存储了具体子类的类名。 (5) Program:客户端测试类 //Program.cs using System; using System.Configuration; using System.Reflection; namespace TemplateMethodSample { class Program { static void Main(string[] args) { Account account; //读取配置文件 string subClassStr =ConfigurationManager.AppSettings["subClass"]; //反射生成对象 account =(Account)Assembly.Load("TemplateMethodSample").CreateInstance(subClassStr); account.Handle("张无忌","123456"); Console.Read(); } } } 结果及分析 编译并运行程序,输出结果如下: 账号:张无忌 密码:123456 按活期利率计算利息! 显示利息! 如果需要更换具体子类,无须修改源代码,只需修改配置文件App.config,例如将活期账户(CurrentAccount)改为定期账户(Saving Account),只需将存储在配置文件中的具体子类CurrentAccount改为SavingAccount,如下代码所示: 重新运行客户端程序,输出结果如下: 账号:张无忌 密码:123456 按定期利率计算利息! 显示利息! 如果需要增加新的具体子类(新的账户类型),原有代码均无须修改,完全符合开闭原则。 4 钩子方法的使用 模板方法模式中,在父类中提供了一个定义算法框架的模板方法,还提供了一系列抽象方法、具体方法和钩子方法,其中钩子方法的引入使得子类可以控制父类的行为。最简单的钩子方法就是空方法,代码如下: public virtual void Display() { } 当然也可以在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。 另一种钩子方法可以实现对其他方法进行约束,这种钩子方法通常返回一个bool类型,即返回true或false,用来判断是否执行某一个基本方法,下面通过一个实例来说明这种钩子方法的使用。 某软件公司欲为销售管理系统提供一个数据图表显示功能,该功能的实现包括如下几个步骤: (1) 从数据源获取数据; (2) 将数据转换为XML格式; (3) 以某种图表方式显示XML格式的数据。 该功能支持多种数据源和多种图表显示方式,但所有的图表显示操作都基于XML格式的数据,因此可能需要对数据进行转换,如果从数据源获取的数据已经是XML数据则无须转换。 由于该数据图表显示功能的三个步骤次序是固定的,且存在公共代码(例如数据格式转换代码),满足模板方法模式的适用条件,可以使用模板方法模式对其进行设计。因为数据格式的不同,XML数据可以直接显示,而其他格式的数据需要进行转换,因此第(2)步“将数据转换为XML格式”的执行存在不确定性,为了解决这个问题,可以定义一个钩子方法IsNotXMLData()来对数据转换方法进行控制。通过分析,该图表显示功能的基本结构如图4所示: 图4 数据图表显示功能结构图 可以将公共方法和框架代码放在抽象父类中,代码如下: //DataViewer.cs using System; namespace TemplateMethodSample { abstract class DataViewer { //抽象方法:获取数据 public abstract void GetData(); //具体方法:转换数据 public void ConvertData() { Console.WriteLine("将数据转换为XML格式。"); } //抽象方法:显示数据 public abstract void DisplayData(); //钩子方法:判断是否为XML格式的数据 public virtual bool IsNotXMLData() { return true; } //模板方法 public void Process() { GetData(); //如果不是XML格式的数据则进行数据转换 if (IsNotXMLData()) { ConvertData(); } DisplayData(); } } } 在上面的代码中,引入了一个钩子方法IsNotXMLData(),其返回类型为bool类型,在模板方法中通过它来对数据转换方法ConvertData()进行约束,该钩子方法的默认返回值为true,在子类中可以根据实际情况覆盖该方法,其中用于显示XML格式数据的具体子类XMLDataViewer代码如下: //XMLDataViewer.cs using System; namespace TemplateMethodSample { class XMLDataViewer : DataViewer { //实现父类方法:获取数据 public override void GetData() { Console.WriteLine("从XML文件中获取数据。"); } //实现父类方法:显示数据,默认以柱状图方式显示,可结合桥接模式来改进 public override void DisplayData() { Console.WriteLine("以柱状图显示数据。"); } //覆盖父类的钩子方法 public override bool IsNotXMLData() { return false; } } } 在具体子类XMLDataViewer中覆盖了钩子方法IsNotXMLData(),返回false,表示该数据已为XML格式,无须执行数据转换方法ConvertData(),客户端代码如下: //Program.cs using System; namespace TemplateMethodSample { class Program { static void Main(string[] args) { DataViewer dv; dv = new XMLDataViewer(); dv.Process(); Console.Read(); } } } 该程序运行结果如下: 从XML文件中获取数据。 以柱状图显示数据。 5 模板方法模式效果与适用场景 模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的模式。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)。 5.1 模式优点 模板方法模式的主要优点如下: (1) 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。 (2) 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。 (3) 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。 (4) 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。 5.2 模式缺点 模板方法模式的主要缺点如下: 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。 5.3 模式适用场景 在以下情况下可以考虑使用模板方法模式: (1) 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。 (2) 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。 (3) 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。 想必大家都去过医院,虽然没有人喜欢去医院(爱岗敬业的医务工作人员除外,微笑)。在医生开具处方单(药单)后,很多医院都存在如下处理流程:划价人员拿到处方单之后根据药品名称和数量计算总价,药房工作人员根据药品名称和数量准备药品,如图26-1所示: 在图26-1中,我们可以将处方单看成一个药品信息的集合,里面包含了一种或多种不同类型的药品信息,不同类型的工作人员(如划价人员和药房工作人员)在操作同一个药品信息集合时将提供不同的处理方式,而且可能还会增加新类型的工作人员来操作处方单。 在软件开发中,有时候我们也需要处理像处方单这样的集合对象结构,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理方式。在设计模式中,有一种模式可以满足上述要求,其模式动机就是以不同的方式操作复杂对象结构,该模式就是我们本章将要介绍的访问者模式。 26.1 OA系统中员工数据汇总 Sunny软件公司欲为某银行开发一套OA系统,在该OA系统中包含一个员工信息管理子系统,该银行员工包括正式员工和临时工,每周人力资源部和财务部等部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等。该公司基本制度如下: (1) 正式员工(Full timeEmployee)每周工作时间为40小时,不同级别、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时,所缺时间按照请假处理,请假所扣工资以80元/小时计算,直到基本工资扣除到零为止。除了记录实际工作时间外,人力资源部需记录加班时长或请假时长,作为员工平时表现的一项依据。 (2) 临时工(Part timeEmployee)每周工作时间不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需记录实际工作时间。 人力资源部和财务部工作人员可以根据各自的需要对员工数据进行汇总处理,人力资源部负责汇总每周员工工作时间,而财务部负责计算每周员工工资。 Sunny软件公司开发人员针对上述需求,提出了一个初始解决方案,其核心代码如下所示: import java.util.*; class EmployeeList { private ArrayList //增加员工 public void addEmployee(Employee employee) { list.add(employee); } //处理员工数据 public void handle(String departmentName) { if(departmentName.equalsIgnoreCase("财务部"))//财务部处理员工数据 { for(Object obj : list) { if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee")) { System.out.println("财务部处理全职员工数据!"); } else { System.out.println("财务部处理兼职员工数据!"); } } } else if(departmentName.equalsIgnoreCase("人力资源部")) //人力资源部处理员工数据 { for(Object obj : list) { if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee")) { System.out.println("人力资源部处理全职员工数据!"); } else { System.out.println("人力资源部处理兼职员工数据!"); } } } } } 在EmployeeList类的handle()方法中,通过对部门名称和员工类型进行判断,不同部门对不同类型的员工进行了不同的处理,满足了员工数据汇总的要求。但是该解决方案存在如下几个问题: (1) EmployeeList类非常庞大,它将各个部门处理各类员工数据的代码集中在一个类中,在具体实现时,代码将相当冗长,EmployeeList类承担了过多的职责,既不方便代码的复用,也不利于系统的扩展,违背了“单一职责原则”。 (2)在代码中包含大量的“if…else…”条件判断语句,既需要对不同部门进行判断,又需要对不同类型的员工进行判断,还将出现嵌套的条件判断语句,导致测试和维护难度增大。 (3)如果要增加一个新的部门来操作员工集合,不得不修改EmployeeList类的源代码,在handle()方法中增加一个新的条件判断语句和一些业务处理代码来实现新部门的访问操作。这违背了“开闭原则”,系统的灵活性和可扩展性有待提高。 (4)如果要增加一种新类型的员工,同样需要修改EmployeeList类的源代码,在不同部门的处理代码中增加对新类型员工的处理逻辑,这也违背了“开闭原则”。 如何解决上述问题?如何为同一集合对象中的元素提供多种不同的操作方式?访问者模式就是一个值得考虑的解决方案,它可以在一定程度上解决上述问题(解决大部分问题)。访问者模式可以为为不同类型的元素提供多种访问操作方式,而且可以在不修改原有系统的情况下增加新的操作方式。 26.2 访问者模式概述 访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。例如处方单中的各种药品信息就是被访问的元素,而划价人员和药房工作人员就是访问者。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。 在使用访问者模式时,被访问元素通常不是单独存在的,它们存储在一个集合中,这个集合被称为“对象结构”,访问者通过遍历对象结构实现对其中存储的元素的逐个操作。 访问者模式定义如下: 访问者模式(Visitor Pattern):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。 访问者模式的结构较为复杂,其结构如图26-2所示: 在访问者模式结构图中包含如下几个角色: ●Vistor(抽象访问者):抽象访问者为对象结构中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。 ●ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。 ●Element(抽象元素):抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法通常以一个抽象访问者作为参数。【稍后将介绍为什么要这样设计。】 ●ConcreteElement(具体元素):具体元素实现了accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作。 ● ObjectStructure(对象结构):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。 访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。访问者模式包括两个层次结构,一个是访问者层次结构,提供了抽象访问者和具体访问者,一个是元素层次结构,提供了抽象元素和具体元素。相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同访问方式访问。在访问者模式中,增加新的访问者无须修改原有系统,系统具有较好的可扩展性。 在访问者模式中,抽象访问者定义了访问元素对象的方法,通常为每一种类型的元素对象都提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的命名一般有两种方式:一种是直接在方法名中标明待访问元素对象的具体类型,如visitElementA(ElementA elementA),还有一种是统一取名为visit(),通过参数类型的不同来定义一系列重载的visit()方法。当然,如果所有的访问者对某一类型的元素的访问操作都相同,则可以将操作代码移到抽象访问者类中,其典型代码如下所示: abstract class Visitor { public abstract void visit(ConcreteElementA elementA); public abstract void visit(ConcreteElementB elementB); public void visit(ConcreteElementC elementC) { //元素ConcreteElementC操作代码 } } 在这里使用了重载visit()方法的方式来定义多个方法用于操作不同类型的元素对象。在抽象访问者Visitor类的子类ConcreteVisitor中实现了抽象的访问方法,用于定义对不同类型元素对象的操作,具体访问者类典型代码如下所示: class ConcreteVisitor extends Visitor { public void visit(ConcreteElementA elementA) { //元素ConcreteElementA操作代码 } public void visit(ConcreteElementB elementB) { //元素ConcreteElementB操作代码 } } 对于元素类而言,在其中一般都定义了一个accept()方法,用于接受访问者的访问,典型的抽象元素类代码如下所示: interface Element { public void accept(Visitor visitor); } 需要注意的是该方法传入了一个抽象访问者Visitor类型的参数,即针对抽象访问者进行编程,而不是具体访问者,在程序运行时再确定具体访问者的类型,并调用具体访问者对象的visit()方法实现对元素对象的操作。在抽象元素类Element的子类中实现了accept()方法,用于接受访问者的访问,在具体元素类中还可以定义不同类型的元素所特有的业务方法,其典型代码如下所示: class ConcreteElementA implementsElement { public void accept(Visitor visitor) { visitor.visit(this); } public void operationA() { //业务方法 } } 在具体元素类ConcreteElementA的accept()方法中,通过调用Visitor类的visit()方法实现对元素的访问,并以当前对象作为visit()方法的参数。其具体执行过程如下: (1) 调用具体元素类的accept(Visitorvisitor)方法,并将Visitor子类对象作为其参数; (2) 在具体元素类accept(Visitorvisitor)方法内部调用传入的Visitor对象的visit()方法,如visit(ConcreteElementA elementA),将当前具体元素类对象(this)作为参数,如visitor.visit(this); (3) 执行Visitor对象的visit()方法,在其中还可以调用具体元素对象的业务方法。 这种调用机制也称为“双重分派”,正因为使用了双重分派机制,使得增加新的访问者无须修改现有类库代码,只需将新的访问者对象作为参数传入具体元素对象的accept()方法,程序运行时将回调在新增Visitor类中定义的visit()方法,从而增加新的元素访问方式。 思考 双重分派机制如何用代码实现? 在访问者模式中,对象结构是一个集合,它用于存储元素对象并接受访问者的访问,其典型代码如下所示: class ObjectStructure { private ArrayList public void accept(Visitor visitor) { Iterator i=list.iterator(); while(i.hasNext()) { ((Element)i.next()).accept(visitor); //遍历访问集合中的每一个元素 } } public void addElement(Element element) { list.add(element); } public void removeElement(Element element) { list.remove(element); } } 在对象结构中可以使用迭代器对存储在集合中的元素对象进行遍历,并逐个调用每一个对象的accept()方法,实现对元素对象的访问操作。 思考 访问者模式是否符合“开闭原则”?【从增加新的访问者和增加新的元素两方面考虑。】 26.3 完整解决方案 Sunny软件公司开发人员使用访问者模式对OA系统中员工数据汇总模块进行重构,使得系统可以很方便地增加新类型的访问者,更加符合“单一职责原则”和“开闭原则”,重构后的基本结构如图26-3所示: 在图26-3中,FADepartment表示财务部,HRDepartment表示人力资源部,它们充当具体访问者角色,其抽象父类Department充当抽象访问者角色;EmployeeList充当对象结构,用于存储员工列表;FulltimeEmployee表示正式员工,ParttimeEmployee表示临时工,它们充当具体元素角色,其父接口Employee充当抽象元素角色。完整代码如下所示: import java.util.*; //员工类:抽象元素类 interface Employee { public void accept(Department handler); //接受一个抽象访问者访问 } //全职员工类:具体元素类 class FulltimeEmployee implementsEmployee { private String name; private double weeklyWage; private int workTime; public FulltimeEmployee(String name,double weeklyWage,int workTime) { this.name = name; this.weeklyWage = weeklyWage; this.workTime = workTime; } public void setName(String name) { this.name = name; } public void setWeeklyWage(double weeklyWage) { this.weeklyWage = weeklyWage; } public void setWorkTime(int workTime) { this.workTime = workTime; } public String getName() { return (this.name); } public double getWeeklyWage() { return (this.weeklyWage); } public int getWorkTime() { return (this.workTime); } public void accept(Department handler) { handler.visit(this); //调用访问者的访问方法 } } //兼职员工类:具体元素类 class ParttimeEmployee implementsEmployee { private String name; private double hourWage; private int workTime; public ParttimeEmployee(String name,double hourWage,int workTime) { this.name = name; this.hourWage = hourWage; this.workTime = workTime; } public void setName(String name) { this.name = name; } public void setHourWage(double hourWage) { this.hourWage = hourWage; } public void setWorkTime(int workTime) { this.workTime = workTime; } public String getName() { return (this.name); } public double getHourWage() { return (this.hourWage); } public int getWorkTime() { return (this.workTime); } public void accept(Department handler) { handler.visit(this); //调用访问者的访问方法 } } //部门类:抽象访问者类 abstract class Department { //声明一组重载的访问方法,用于访问不同类型的具体元素 public abstract void visit(FulltimeEmployee employee); public abstract void visit(ParttimeEmployee employee); } //财务部类:具体访问者类 class FADepartment extends Department { //实现财务部对全职员工的访问 public void visit(FulltimeEmployee employee) { int workTime = employee.getWorkTime(); double weekWage = employee.getWeeklyWage(); if(workTime > 40) { weekWage = weekWage + (workTime - 40) * 100; } else if(workTime < 40) { weekWage = weekWage - (40 - workTime) * 80; if(weekWage < 0) { weekWage = 0; } } System.out.println("正式员工" +employee.getName() + "实际工资为:" + weekWage +"元。"); } //实现财务部对兼职员工的访问 public void visit(ParttimeEmployee employee) { int workTime = employee.getWorkTime(); double hourWage = employee.getHourWage(); System.out.println("临时工" +employee.getName() + "实际工资为:" + workTime *hourWage + "元。"); } } //人力资源部类:具体访问者类 class HRDepartment extends Department { //实现人力资源部对全职员工的访问 public void visit(FulltimeEmployee employee) { int workTime = employee.getWorkTime(); System.out.println("正式员工" +employee.getName() + "实际工作时间为:" + workTime +"小时。"); if(workTime > 40) { System.out.println("正式员工" +employee.getName() + "加班时间为:" + (workTime -40) + "小时。"); } else if(workTime < 40) { System.out.println("正式员工" +employee.getName() + "请假时间为:" + (40 -workTime) + "小时。"); } } //实现人力资源部对兼职员工的访问 public void visit(ParttimeEmployee employee) { int workTime = employee.getWorkTime(); System.out.println("临时工" +employee.getName() + "实际工作时间为:" + workTime +"小时。"); } } //员工列表类:对象结构 class EmployeeList { //定义一个集合用于存储员工对象 private ArrayList public void addEmployee(Employee employee) { list.add(employee); } //遍历访问员工集合中的每一个员工对象 public void accept(Department handler) { for(Object obj : list) { ((Employee)obj).accept(handler); } } } 为了提高系统的灵活性和可扩展性,我们将具体访问者类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示: import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.SAXException; import java.io.*; class XMLUtil { //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象 public static Object getBean() { try { //创建文档对象 DocumentBuilderFactory dFactory =DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File("config.xml")); //获取包含类名的文本节点 NodeList nl = doc.getElementsByTagName("className"); Node classNode=nl.item(0).getFirstChild(); String cName=classNode.getNodeValue(); //通过类名生成实例对象并将其返回 Class c=Class.forName(cName); Object obj=c.newInstance(); return obj; } catch(Exception e) { e.printStackTrace(); return null; } } } 配置文件config.xml中存储了具体访问者类的类名,代码如下所示: 编写如下客户端测试代码: class Client { public static void main(String args[]) { EmployeeList list = new EmployeeList(); Employee fte1,fte2,fte3,pte1,pte2; fte1 = new FulltimeEmployee("张无忌",3200.00,45); fte2 = new FulltimeEmployee("杨过",2000.00,40); fte3 = new FulltimeEmployee("段誉",2400.00,38); pte1 = new ParttimeEmployee("洪七公",80.00,20); pte2 = new ParttimeEmployee("郭靖",60.00,18); list.addEmployee(fte1); list.addEmployee(fte2); list.addEmployee(fte3); list.addEmployee(pte1); list.addEmployee(pte2); Department dep; dep = (Department)XMLUtil.getBean(); list.accept(dep); } } 编译并运行程序,输出结果如下: 正式员工张无忌实际工资为:3700.0元。 正式员工杨过实际工资为:2000.0元。 正式员工段誉实际工资为:2240.0元。 临时工洪七公实际工资为:1600.0元。 临时工郭靖实际工资为:1080.0元。 如果需要更换具体访问者类,无须修改源代码,只需修改配置文件,例如将访问者类由财务部改为人力资源部,只需将存储在配置文件中的具体访问者类FADepartment改为HRDepartment,如下代码所示: 重新运行客户端程序,输出结果如下: 正式员工张无忌实际工作时间为:45小时。 正式员工张无忌加班时间为:5小时。 正式员工杨过实际工作时间为:40小时。 正式员工段誉实际工作时间为:38小时。 正式员工段誉请假时间为:2小时。 临时工洪七公实际工作时间为:20小时。 临时工郭靖实际工作时间为:18小时。 如果要在系统中增加一种新的访问者,无须修改源代码,只要增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合“开闭原则”。 如果要在系统中增加一种新的具体元素,例如增加一种新的员工类型为“退休人员”,由于原有系统并未提供相应的访问接口(在抽象访问者中没有声明任何访问“退休人员”的方法),因此必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了“开闭原则”。 综上所述,访问者模式与抽象工厂模式类似,对“开闭原则”的支持具有倾斜性,可以很方便地添加新的访问者,但是添加新的元素较为麻烦。 26.4 访问者模式与组合模式联用 在访问者模式中,包含一个用于存储元素对象集合的对象结构,我们通常可以使用迭代器来遍历对象结构,同时具体元素之间可以存在整体与部分关系,有些元素作为容器对象,有些元素作为成员对象,可以使用组合模式来组织元素。引入组合模式后的访问者模式结构图如图26-4所示: 需要注意的是,在图26-4所示结构中,由于叶子元素的遍历操作已经在容器元素中完成,因此要防止单独将已增加到容器元素中的叶子元素再次加入对象结构中,对象结构中只保存容器元素和孤立的叶子元素。 26.5 访问者模式总结 由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,因此在实际应用中使用频率不是特别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在XML文档解析、编译器的设计、复杂集合对象的处理等领域访问者模式得到了一定的应用。 1.主要优点 访问者模式的主要优点如下: (1) 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。 (2) 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。 (3) 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。 2.主要缺点 访问者模式的主要缺点如下: (1) 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。 (2) 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。 3.适用场景 在以下情况下可以考虑使用访问者模式: (1) 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。 (2) 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。 (3) 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 练习 Sunny软件公司欲为某高校开发一套奖励审批系统,该系统可以实现教师奖励和学生奖励的审批(Award Check),如果教师发表论文数超过10篇或者学生论文超过2篇可以评选科研奖,如果教师教学反馈分大于等于90分或者学生平均成绩大于等于90分可以评选成绩优秀奖。试使用访问者模式设计该系统,以判断候选人集合中的教师或学生是否符合某种获奖要求。 仅供个人学习,如有抄袭请包容.....6) 自定义语言的实现——解释器模式(六)
四、 迭代器模式-Iterator Pattern
1) 遍历聚合对象中的元素——迭代器模式(一)
2) 遍历聚合对象中的元素——迭代器模式(二)
3) 遍历聚合对象中的元素——迭代器模式(三)
4) 遍历聚合对象中的元素——迭代器模式(四)
5) 遍历聚合对象中的元素——迭代器模式(五)
6) 遍历聚合对象中的元素——迭代器模式(六)
五、 中介者模式-Mediator Pattern
1) 协调多个对象之间的交互——中介者模式(一)
2) 协调多个对象之间的交互——中介者模式(二)
3) 协调多个对象之间的交互——中介者模式(三)
4) 协调多个对象之间的交互——中介者模式(四)
5) 协调多个对象之间的交互——中介者模式(五)
六、 备忘录模式-Memento Pattern
1) 撤销功能的实现——备忘录模式(一)
2) 撤销功能的实现——备忘录模式(二)
3) 撤销功能的实现——备忘录模式(三)
4) 撤销功能的实现——备忘录模式(四)
5) 撤销功能的实现——备忘录模式(五)
七、 观察者模式-Observer Pattern
1) 对象间的联动——观察者模式(一)
2) 对象间的联动——观察者模式(二)
3) 对象间的联动——观察者模式(三)
4) 对象间的联动——观察者模式(四)
5) 对象间的联动——观察者模式(五)
6) 对象间的联动——观察者模式(六)
八、 状态模式-State Pattern
1) 处理对象的多种状态及其相互转换——状态模式(一)
2) 处理对象的多种状态及其相互转换——状态模式(二)
3) 处理对象的多种状态及其相互转换——状态模式(三)
4) 处理对象的多种状态及其相互转换——状态模式(四)
5) 处理对象的多种状态及其相互转换——状态模式(五)
6) 处理对象的多种状态及其相互转换——状态模式(六)
九、 策略模式-Strategy Pattern
1) 算法的封装与切换——策略模式(一)
2) 算法的封装与切换——策略模式(二)
3) 算法的封装与切换——策略模式(三)
4) 算法的封装与切换——策略模式(四)
十、 模板方法模式-Template Method Pattern
1) 模板方法模式深度解析(一)
2) 模板方法模式深度解析(二)
3) 模板方法模式深度解析(三)
十一、 访问者模式-Visitor Pattern
1) 操作复杂对象结构——访问者模式(一)
2) 操作复杂对象结构——访问者模式(二)
3) 操作复杂对象结构——访问者模式(三)
4) 操作复杂对象结构——访问者模式(四)