从击鼓传花谈起
击鼓传花是一种热闹而又紧张的饮酒游戏。在酒宴上宾客依次坐定位置,由一人击鼓,击鼓的地方与传花的地方是分开的,以示公正。开始击鼓时,花束就开始依次传递,鼓声一落,如果花束在某人手中,则该人就得饮酒。
假比说,贾母、贾赦、贾政、贾宝玉和贾环是五个参加击鼓传花游戏的传花者,他们组成一个环链。击鼓者将花传给贾母,开始传花游戏。花由贾母传给贾赦,由贾赦传给贾政,由贾政传给贾宝玉,又由贾宝玉传给贾环,由贾环传回给贾母,如此往复(见下图)。当鼓声停止时,手中有花的人就得执行酒令。
击鼓传花便是责任链模式的应用。在责任链模式里,很多的对象由每一个对象对其下家的引用而联接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
责任链可能是一条直线、一个环链甚至一个树结构的一部分。
责任链模式的结构
责任链模式是一种对象的行为模式,它所涉及到的角色如下:
第一、抽象处理者(Handler)角色、定义出一个处理请求的接口;如果需要,接口可以定义出一个方法,以返回对下家的引用。下图给出了一个示意性的类图:
在图中的积累关系给出了具体子类对下家的引用,抽象方法handleRequest()规范了子类处理请求的操作。
第二、具体处理者(ConcreteHandler)角色、处理接到请求后,可以选择将请求处理掉,或者将请
求传给下家。下图给出了一个示意性的类图。
上图中的示意性的具体处理者ConcreteHandler类只有handleRequest()一个方法。
责任链模式的静态类结构可见下图:
在图中还给出了一个客户端,以便读者可以更清楚地看到责任链模式是怎样应用的。抽象处理者的示意性源代码:
public class Handler
{
public void handleRequest()
{
if (successor != null)
{
successor.handleRequest();
}
// Write your code here
}
public void setSuccessor(Handler successor)
{
this.successor = successor;
}
public Handler getSuccessor()
{
return successor;
}
private Handler successor;
} |
具体处理者的示意性源代码:
public class ConcreteHandler extends Handler
{
public void handleRequest()
{
if (getSuccessor() != null)
{
getSuccessor().handleRequest();
}
if (successor != null)
{
successor.handleRequest();
}
// Write your code here
}
} |
客户端的源代码如下:
public class Client
{
private Handler handler;
public static void main(String[] args)
{
handler = new ConcreteHandler();
//write your code here
}
} |
纯的与不纯的责任链模式
一个纯的责任链模式要求一个具体的处理者对象只能在两个行为中选择一个:一是承担责任,二是把责任推给下家。不允许出现某一个具体处理者对象在承担了一部分责任后又把责任向下传的情况。
在一个纯的责任链模式里面,一个请求必须被某一个处理者对象所接受;在一个不纯的责任链模式里面,一个请求可以最终不被任何接受端对象所接受。
纯的责任链模式的实际例子很难找到,一般看到的例子均是不纯的责任链模式的实现。有些人认为不纯的责任链根本不是责任链模式,这也许是有道理的;但是在实际的系统里,纯的责任链很难找到;如果坚持责任链不纯便不是责任链模式,那么责任链模式便不会有太大的意义了。
Java1.0版的AWT事件处理机制
Java的1.0版中AWT库使用了责任链模式和命令模式来处理GUI的事件。由于视窗部件往往处在容器部件里面,因此当事件发生在一个部件上时,此部件的事件处理器可以处理此事件,然后决定是否将事件向上级容器部件传播;上级容器部件接到事件后可以在此处理此事件然后决定是否将事件再次向上级容器部件传播,如此往复,直到事件到达顶层部件。
事件浮升机制
比如,当一个视窗部件接到一个MOUSE_CLICKED事件时,事件首先传播到它所发生的部件上,然后向其容器部件传播。容器可以选择处理这个事件,或者再将此事件向更高一级的容器部件传播。事件如此一级级地向上传播,就像水底的气泡一点一点地冒到水面上一样,因此又叫做事件浮升(Event
Bubbling)机制。下面就是一段典型的Java1.0版的AWT库里处理事件的代码:
public boolean action(Event event, Object obj)
{
if (event.target == btnOK)
{
doOKBtnAction();
}
else if (event.target == btnExit)
{
doExitBtnAction();
}
else
{
return super.action(event, obj);
}
return true;
} |
在这段代码里面,action()判断目标部件是不是btnOK或btnExit;如果是,便运行相应的方法;如果不是,便返还true。一个方法返还true便使得事件停止浮升。
AWT1.0的事件处理的模型的缺点之一
AWT1.0的事件处理的模型是基于继承的。为了使一个程序能够捕捉GUI的事件并处理此事件,必须subclass此部件并且给其子类配备事件处理器,也就是置换掉action()方法或者handleEvent()方法。这不是应当提倡的做法:在一个面向对象的系统里,经常使用的应当是委派,继承不应当是常态。
在一个复杂的GUI系统里,这样为所有有事件的部件提供子类,会导致很多的子类,这是不是很麻烦的吗?
当然,由于事件浮升机制,可以在部件的树结构的根部部件里面处理所有的事件。但是这样一来,就需要使用复杂的条件转移语句在这个根部部件里辨别事件的起源和处理方法。这种非常过程化的处理方法很难维护,并且与面向对象的设计思想相违背。
AWT1.0的事件处理的模型的缺点之二
由于每一个事件都会沿着部件树结构向上传播,因此事件浮升机制会使得事件的处理变得较慢。这也是缺点之一。
比如在有些操作系统中,鼠标每移动一个色素,都会激发一个MOUSE_MOVE事件。每一个这样的事件都会沿着部件的容器树结构向上传播,这会使得鼠标事件成灾。
AWT1.0的事件处理的模型的缺点之三
AWT1.0的事件处理的模型只适用于AWT部件类。这是此模型的另一个缺点。
责任链模式要求链上所有的对象都继承自一个共同的父类,这个类便是java.awt.Component类。
AWT1.0的事件处理的模型是不纯的责任链模式
显然,由于每一级的部件在接到事件时,都可以处理此事件;而不论此事件是否在这一级得到处理,事件都可以停止向上传播或者继续向上传播。这是典型的不纯的责任链模式。
AWT1.1以后的事件处理的模型
自从AWT1.1以后,AWT的事件处理模型于1.0相比有了很大的变化。新的事件处理模型是建立在观察者模式的基础之上的,而不再是责任链模式的基础之上的。
关于新的事件处理模型和观察者设计模式,请见“观察者模式”一节。
红楼梦中击鼓传花的故事
显然,击鼓传花符合责任链模式的定义。参加游戏的人是一个个的具体处理者对象,击鼓的人便是客户端对象。花代表酒令,是传向处理者的请求,每一个参加游戏的人在接到传来的花时,可选择的行为只有两个:一是将花向下传;一是执行酒令---喝酒。一个人不能既执行酒令,又向下家传花;当某一个人执行了酒令之后,游戏重新开始。击鼓的人并不知道最终是由哪一个做游戏的人执行酒令,当然执行酒令的人必然是做游戏的人们中的一个。
击鼓传花的类图结构如下:
单独考虑击鼓传花系统,那么像贾母、贾赦、贾政、贾宝玉和贾环等传花者均应当是“具体传花者”的对象,而不应当是单独的类;但是责任链模式往往是建立在现有系统的基础之上的,因此链的结构和组成不由责任链模式本身决定。
系统的分析
在《红楼梦》第七十五回里生动地描述了贾府里的一场击鼓传花游戏:“贾母坐下,左垂首贾赦,贾珍,贾琏,贾蓉,右垂首贾政,宝玉,贾环,贾兰,团团围坐。...贾母便命折一枝桂花来,命一媳妇在屏后击鼓传花。若花到谁手中,饮酒一杯...于是先从贾母起,次贾赦,一一接过。鼓声两转,恰恰在贾政手中住了,只得饮了酒。”这场游戏接着又把花传到了宝玉和贾赦手里,接着又传到了在贾环手里...
如果用一个对象系统描述贾府,那么贾母、贾赦、贾政、贾宝玉和贾环等等就应当分别由一个个具体类代表,而这场击鼓传花游戏的类图,按照责任链模式,应当如下图所示:
换言之,在击鼓传花游戏里面,有下面的几种角色:
- 抽象传花者,或Handler角色、定义出参加游戏的传花人要遵守的规则,也就是一个处理请求的接口 和对下家的引用;
- 具体传花者,或ConcreteHandler角色、每一个传花者都知道下家是谁,要么执行酒令,要么把花 向下传。这个角色由贾母、贾赦、贾珍、贾琏、贾蓉、贾政、宝玉、贾环、贾兰等扮演。
- 击鼓人,或Client角色、即行酒令的击鼓之人。《红楼梦》没有给出此人的具体姓名,只是说由“一 媳妇”扮演。
可以看出,击鼓传花游戏满足责任链模式的定义,是纯的责任链模式的例子。
Java系统的解
下面的类图给出了这些类的具体接口设计。读者不难看出,DrumBeater(击鼓者)、Player(传花者)、JiaMu(贾母)、JiaShe(贾赦)、JiaZheng(贾政)、JiaBaoYu(宝玉)、JiaHuan(贾环)等组成这个系统。
下面是客户端类DrumBeater的源代码:
public class DrumBeater
{
private static Player player;
static public void main(String[] args)
{
player = new JiaMu( new JiaShe( new JiaZheng( new JiaBaoYu(new
JiaHuan(null)))));
player.handle(4);
}
} |
abstract class Player
{
abstract public void handle(int i);
private Player successor;
public Player() { successor = null;
}
protected void setSuccessor(Player aSuccessor)
{
successor = aSuccessor;
}
public void next(int index)
{
if( successor != null )
{
successor.handle(index);
}
else
{
System.out.println("Program terminated.");
}
}
} |
抽象类Player给出了两个方法的实现,以格式setSuccessor(),另一个是next()。前者用来设置一个传花者对象的下家,后者用来将酒令传给下家。Player类给出了一个抽象方法handle(),代表执行酒令。
下面的这些具体传花者类将给出handle()方法的实现。
class JiaMu extends Player
{
public JiaMu(Player aSuccessor)
{
this.setSuccessor(aSuccessor);
}
public void handle(int i)
{
if( i == 1 )
{
System.out.println("Jia Mu gotta drink!");
}
else
{
System.out.println("Jia Mu passed!"); next(i);
}
}
} |
class JiaShe extends Player
{
public JiaShe(Player aSuccessor)
{
this.setSuccessor(aSuccessor);
}
public void handle(int i)
{
if( i == 2 )
{
System.out.println("Jia She gotta drink!");
}
else
{
System.out.println("Jia She passed!");
next(i);
}
}
} |
class JiaZheng extends Player
{
public JiaZheng(Player aSuccessor)
{
this.setSuccessor(aSuccessor);
}
public void handle(int i)
{
if( i == 3 )
{
System.out.println("Jia Zheng gotta drink!");
}
else
{
System.out.println("Jia Zheng passed!");
next(i);
}
}
} |
class JiaBaoYu extends Player
{
public JiaBaoYu(Player aSuccessor)
{
this.setSuccessor(aSuccessor);
}
public void handle(int i)
{
if( i == 4 )
{
System.out.println("Jia Bao Yu gotta drink!");
}
else
{
System.out.println("Jia Bao Yu passed!");
next(i);
}
}
} |
class JiaHuan extends Player
{
public JiaHuan(Player aSuccessor)
{
this.setSuccessor(aSuccessor);
}
public void handle(int i)
{
if( i == 5 )
{
System.out.println("Jia Huan gotta drink!");
}
else
{
System.out.println("Jia Huan passed!");
next(i);
}
}
} |
可以看出,DrumBeater设定了责任链的成员和他们的顺序:责任链由贾母开始到贾环,周而复始。JiaMu类、JiaShe类、JiaZheng类、JiaBaoYu类与JiaHuan类均是抽象传花者Player类的子类。
本节所实现的DrumBeater类在把请求传给贾母时,实际上指定了由4号传花者处理酒令。虽然DrumBeater并不知道哪一个传花者类持有号码4,但是这个号码在本系统一开始就写死的。这当然并不符合击鼓传花游戏的精神,因为这个游戏实际上要求有两个同时进行的过程:击鼓过程和传花过程。击鼓应当是定时停止的,当击鼓停止时,执行酒令者就确定了。但是本节这样做可以使问题得到简化并将读者的精力放在责任链模式上,而不是两个过程的处理上。
下一章会给出一个多线程的系统,更加逼真地模拟击鼓传花系统。
在什么情况下使用责任链模式
在下面的情况下使用责任链模式:
第一、系统已经有一个由处理者对象组成的链。这个链可能由复合模式给出,
第一、当有多于一个的处理者对象会处理一个请求,而且在事先并不知道到底由哪一个处理者对象处理一个请求。这个处理者对象是动态确定的。
第二、当系统想发出一个请求给多个处理者对象中的某一个,但是不明显指定是哪一个处理者对象会处理此请求。
第三、当处理一个请求的处理者对象集合需要动态地指定时。
使用责任链模式的长处和短处
责任链模式减低了发出命令的对象和处理命令的对象之间的耦合,它允许多与一个的处理者对象根据自己的逻辑来决定哪一个处理者最终处理这个命令。换言之,发出命令的对象只是把命令传给链结构的起始者,而不需要知道到底是链上的哪一个节点处理了这个命令。
显然,这意味着在处理命令上,允许系统有更多的灵活性。哪一个对象最终处理一个命令可以因为由那些对象参加责任链、以及这些对象在责任链上的位置不同而有所不同。
责任链模式的实现
链结构的由来
值得指出的是,责任链模式并不创建出责任链。责任链的创建必须有系统的其它部分完成。
责任链模式减低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。一个链可以是一条线,一个树,也可以是一个环。链的拓扑结构可以是单连通的或多连通的,责任链模式并不指定责任链的拓扑结构。但是责任链模式要求在同一个时间里,命令只可以被传给一个下家(或被处理掉);而不可以传给多于一个下家。在下面的图中,责任链是一个树结构的一部分。
责任链的成员往往是一个更大的结构的一部分。比如在前面所讨论的《红楼梦》中击鼓传花的游戏中,所有的成员都是贾府的成员。如果责任链的成员不存在,那么为了使用责任链模式,就必须创建它们;责任链的具体处理者对象可以是同一个具体处理者类的实例。
在Java的1.0版的AWT事件处理模型里,责任链便是视窗上的部件的容器等级结构。
在下面会谈到的Internet Explorer的DHTML的DOM事件处理模型里,责任链则是DOM等级结构本身。
命令的传递
在一个责任链上传递的可能不只有一个命令,而是数个命令。这些命令可以采取抽象化层、具体化层的多态性实现方式,见下图,从而可以将命令对象与责任链上的对象之间的责任分隔开,并将命令对象与传播命令的对象分隔开。
当然如果责任链上的传播的命令只有一个、且是固定的命令,那么这个命令不一定要对象化。这就是本节处理击鼓传花游戏里面传来传去的花束的办法。花束代表酒令,可以由一个对象代表;但是本章的处理是过程式的,用对下家对象的next()方法的调用达成。
对象的树结构
在面向对象的技术里,对象的树结构是一个强有力的工具,更是模式理论的一个重要的组成部分,需要应用到符合模式、装饰模式和迭代子模式。
《墨子.天志》说:“庶人竭力从事,未得次己而为政,有士政之,士竭力从事,未得次己而为政,有将军、大夫政之;将军、大夫竭力从事,未得次己而为政,有三公、诸侯政之;三公、诸侯竭力听治,未得次己而为政,有天子政之;天子未得次己而为政,有天政之。”
“次”意为恣意。上面的话就是说,百姓有官吏管治,官吏由将军和士大夫管治,将军和士大夫由三公和诸侯管治,三公和诸侯由天子管治,天子由天管治。
当一个百姓提出要求时,此要求会传达到“士”一级,再到“大夫”一级,进而传到“诸侯”一级,“天子”一级,最后到“天”一级。
DHTML中的事件处理
浏览器的DOM(Document Object Model)模型中的事件处理均采用责任链模式。本节首先考察Netscape浏览器的DHTML的事件处理,然后再研究Internet
Explorer的事件模型。
Netscape的事件模型
Netscape的事件处理机制叫做“事件捕捉”(Event
Capturing)。在事件捕捉机制里面,一个事件是从DOM的最高一层向下传播,也就是说,window对象是第一个接到事件的,然后是document对象,如此往下---事件的产生对象反而是最后一个接到事件的。
如果要是一个对象捕获某一个事件,只需要调用captureEvent()方法;如果要使一个对象把某一个事件向下传而不处理此事件,只需要对此对象使用releaseEvents方法即可。下面考察一个简单的事件捕获和传递的例子。
在这个例子里,有一个textbox和两个button,一个叫做“Capture
Event”,单击后会使网页的click事件被捕捉,文字框中的计数会加一;另一个叫做“Release
Event”,单击后会使网页的click事件不被捕捉。
使click事件被捕捉需要调用captureEvent()方法,而使click事件不被捕捉需要调用releaseEvent()方法。下面是具体的html和JavaScript代码。
显然,一个事件可以在几个不同的等级上得到处理,这是一个不纯的责任链模式。
Internet Explorer的事件模型
Internet Explorer处理事件的方式与Netscape既相似又不同。当一个事件发生在Internet
Explorer所浏览的网页中时,Internet Explorer会使用DHTML的“Event
Bubbling”即事件浮升机制处理此事件。Internet Explorer的DOM模型是html对象等级结构和事件处理机制。在DOM里面,每一个html标示都是一个DOM对象,而每一个DOM对象都可以产生事先定义好的几个事件中的一个(或几个)。这样的一个事件会首先发生在事件所属的对象上,然后向上传播,传到此对象所属的容器对象上,如此等等。因此,事件浮升机制恰恰是事件捕捉机制的相反面。
在Event Bubbling机制里面,产生事件的对象首先会收到事件。然后,事件会依照对象的等级结构向上传播。比如一个DIV里有一个Form,Form里面又有一个Button,那么当Button的onclick事件产生时,Form的onclick事件代码就会被执行。然后,事件就会传到DIV对象。如果DIV对象的onclick事件有任何代码的话,这代码就会被执行,然后事件继续沿着DOM结构上行。
如果要阻止事件继续向上传播,可以在事件链的任何一个节点上把cancelBubble性质设置成True即可。
Internet Explorer 浏览器几乎为所有的 HTML
标识符都提供了事件句柄,因此Internet Explorer不需要captureEvents()方法和releaseEvents()方法来捕获和释放事件。下面的JavaScript语句指定了document对象的onclick事件的处理方法:
document.onclick = functionName; |
而下面的语句则停止了document对象对onclick事件的处理。
因为事件处理性质被赋值null,document便没有任何的方法处理此事件。换言之,null值禁止了此对象的事件处理。这种方法可以用到任何的对象和任何的事件上面。当然这一做法不适用于Netscape。
与Netscape中一样,一个事件处理方法可以返还Boolean值。比如,单击一个超链接标记符是否造成浏览器跟进,取决于此超链接标记符的onclick事件是否返还true。
为了显示Internet Explorer中的事件浮升机制,本节特准备了下面的例子。一个Form里面有一个Button,请见下图:
其HTML代码请见下面:
当myButton的onclick事件发生时,myButton的事件处理首先被激发,从而显示出如下的对话窗:
然后事件会象气泡一样浮升到上一级的对象,即myForm对象上。myForm对象的事件处理给出下面的对话窗:
这以后事件继续浮升到更上一级的对象,即body上。这时,document对象的事件处理被激发,并给出下面的对象窗:
这就是事件浮升(Event Bubbling)机制。
显然,这三级对象组成一个责任链,而事件便是命令或请求。当事件沿着责任链传播时,责任链上的对象可以选择处理或不处理此事件;不论事件在某一个等级上是否得到处理,事件都可以停止上浮或继续上浮。这是不纯的责任链模式。
责任链模式与其它模式的关系
责任链模式与以下的设计模式相关: 复合模式(Composite Pattern) 当责任链模式中的对象链属于一个较大的结构时,这个较大的结构可能符合复合模式。
命令模式(Command Pattern) 责任链模式使一个特定的请求接收对象对请求或命令的执行变得不确定。而命令模式使得一个特定的对象对一个命令的执行变得明显和确定。
模版方法模式(Template Method) 当组成责任链的处理者对象是按照复合模式组成一个较大的结构的责成部分的话,模版方法模式经常用来组织单个的对象的行为。
问答题 第一题、在称为“拱猪”的纸牌游戏中,四个参加者中由“猪”牌的,可以选择一个时机放出这张“猪”牌。“猪”牌放出后,四个人中的一个会不可避免地拿到这张“猪”牌。
请使用责任链模式说明这一游戏,并给出UML结构图。 第二题、《墨子.迎敌祠》里描守城军队的结构:“城上步一甲、一戟,其赞三人。五步有伍长,十步有什长,百步有佰长,旁有大帅,中有大将,皆有司吏卒长。”
一个兵勇需要上级批准以便执行一项任务,他要向伍长请求批准。伍长如果有足够的权限,便会批准或驳回请求;如果他没有足够的权限,便会向上级,即什长转达这个请求。什长便会重复同样的过程,直到大将那里。一个请求最终会被批准或驳回,然后就会象下传,直到传回到发出请求的士兵手里。
有些请求会很快返回,有些则要经过较长的过程。请求到底由谁批准,事前并不知道。请求的处理者并不是固定的,有些军官会晋升,转业,或从别的单位转过来,等等。
请使用责任链模式解释这个核准请求的结构。
(本例子受到文献[ALPERT98]里“Chain of Responsibility”一节所给出的一个例子的启发。)
第三题、王羲之在《兰亭序》中写道:“有清流激湍,映带左右,引以为流觞曲水,列坐其次。”讲的是大伙列坐水畔,随水流放下带羽毛的酒杯饮酒。远道而来的酒杯流到谁的面前,谁就取而饮之。
在这个活动中,参加者做成一排,面对着一条弯曲的小溪。侍者把酒杯盛满酒,让酒杯沿着小溪向下漂流。酒杯漂到一个参加者面前的时候,他可以选择取酒饮之,也可以选择让酒杯漂向下家。
假设每一杯酒最终都会被参加者中之一喝掉,那么这个游戏是不是纯的责任链模式?
问答题答案
第一题答案、这是一个纯的责任链模式。
首先,在“猪”牌放出之后,每个人都只能要么躲过“猪”牌,要么吃住“猪”牌。“猪”牌便是责任链模式中的请求,四个人便是四个处理者对象,组成责任链。
每一个参加者的行为不仅仅取决于他手中的牌,而且取决于他是否想得“猪”牌。一个想收全红的人,可能会权力揽“猪”牌,一个不想收全红的人,一般不想收“猪”牌,除非他想阻止别人收“猪”牌。因为一旦有人收全红,另外三个人就会复出较大的代价,因此阻止别人收全红的动机,会促使一个参与者主动收“猪”牌。有的时候,放出“猪”牌的人也会想要得“猪”牌而得不到,有的时候放出“猪”牌的人想要害人但却害了自己。
这就是说,到底是四个人中的哪一个人得到“猪”牌是完全动态决定的。
系统的UML结构图如下:
由于玩牌的时候,可能有四人位置的任意调换,或者有候补者在旁等待,一旦在任的玩家被淘汰,便可上任。这样四个人组成的牌局是动态变化的。同时因为谁会拿到“猪”牌在每一局均会不同,因此谁会放出“猪”牌也是动态的。
因此,责任链的组成和顺序变不是一成不变的,而是动态的和变化的。 第二题答案、墨子的守城部队的等级结构可以用下面的对象图表示。
显然,这是一个纯的责任链模式。任何提出申请的兵勇便是客户端,伍长、什长、佰长、大帅和大将是责任链的具体处理者对象。一个申请会在链上传播,直到某一级的有合适的权限的军官处理申请为止。每一个申请必会得到处理,批准或驳回。一个被处理过的申请会按照相反的方向传播,直到传回到发出申请的兵勇手中。
发出申请的士兵在发出申请时根本不知道他的申请会向上传播多少等级。 第三题答案、这是纯的责任链模式。
首先,酒便是请求的代表。每一个酒会的参与者都是一个请求的处理者对象,所有的参加者组成责任链。一个酒杯会漂过每一个参加者,代表一个请求经过每一个请求处理者对象。
每一个酒会的参加者都有可能选择喝掉某一杯酒,或者让酒继续漂向下一个参加者,而且假定所有的酒最后都会被某一个参加者喝掉,因此这是纯的责任链模式。 |
|