概述
有时需要向对象发送请求,但是不知道 "被请求的操作" 或 "请求的接受者" 的任何信息。在面向过程的程序设计语言中,这类通信是通过回调函数来完成的:在某个地方登记这个函数,然后在后面调用它。在面向对象程序中,command(命令)与回调函数等价,它封装了回调函数。本文演示如何在java中实现command模式。
---------------------------------------------------------------------------
设计模式不但可以加速面向对象工程的设计进度,而且可以提高开发小组的产出以及软件的质量。commad模式是一种对象行为模式,它可以对发送者(sender)和接收者(receiver)完全解耦(decoupling)。("发送者" 是请求操作的对象,"接收者" 是接收请求并执行某操作的对象。有了 "解耦",发送者对接收者的接口一无所知。)这里,"请求"(request)这个术语指的是要被执行的命令。command模式还让我们可以对 "何时" 以及 "如何" 完成请求进行改变。因此,command模式为我们提供了灵活性和可扩展性。
在象c这样的程序设计语言中,函数指针常被用来消除庞大的switch语句。(参见 "java tip 30: polymorphism and java" 获得更详细的介绍)java没有函数指针,所以我们可以用command模式来实现回调函数。在下面的第一个代码示例testcommand.java中,你将实际看到它是如何实现的。
一些开发者在其它语言中已经习惯于使用函数指针,因而他们往往禁不起诱惑,想以同样的方法使用reflection api的method对象。例如,在 "java reflection" 一文中,paul tremblett介绍了如何使用reflection而不是switch语句来实现事务处理(transaction) (参见 "相关资源" 获得tremblett的文章和sun的reflection教程网址的链接)。我是不会为这样的诱惑所动的。正如sun所指出的:在有其它更贴近于java程序设计语言的工具满足使用要求的情况下,一般不提倡使用reflect api。不使用method对象,程序会更易于调试和维护。所以,你应该定义一个接口,并在类中实现它,以执行所需操作。
因此我建议,可以使用command模式并结合java的动态加载和绑定机制来实现函数指针。(关于java的动态加载和绑定机制的详细介绍,参见gosling和henry mcgilton的"the java language environment -- a white paper",列于"相关资源"。)
遵循上面的建议,我们可以运用command模式,利用它所提供的多态性来消除庞大的switch语句,从而设计出可扩展的系统。我们还可以利用java独有的动态加载和绑定机制来构筑动态的、并且可以动态扩展的系统。这一点在下面第二个代码示例testtransactioncommand.java中进行说明。
command模式使请求本身成为一个对象。这个对象和其它对象一样可以被存储和四处传递。这种模式的关键在于一个command接口:它声明了一个接口,用于执行操作。最简单的形式下,这个接口包含一个抽象的execute操作。每个具体的command类把接收者作为一个实例变量进行存储,从而指定了一对 "接收者" 和 "行为"。它为execute()方法提供不同的实现以进行请求调用。接收者知道如何执行请求。
如下的图1表示了switch--一个command对象的集合体(aggregation)。它的接口中有flipup()和flipdown()两种操作。switch被称为 "调用者"(invoker),因为它调用command接口中的execute操作。
具体的command,如lightoncommand,实现command接口的execute操作。它知道去调用合适的接收者对象的操作。这种情况下它充当了一个适配器(adapter)。通过 "适配器"这一术语, 我想说明:具体的command对象是一个简单的连接器,它连接具有不同接口的 "调用者" 和 "接收者"。
客户实例化调用者,接收者以及具体的command对象。
图2为时序图,它表示对象间的相互作用。它说明了command如何对调用者和接收者(以及它执行的请求)解耦。客户用合适的接收者作为构造函数的参数来创建具体的command。然后,它将command保存在调用者中。调用者回调具体的command,后者知道如何完成想要的action()操作。
客户(代码中的主程序)创建具体的command对象并设置它的接收者。作为一个调用者对象,switch保存具体的command对象。调用者通过对command对象调用execute来发送请求。具体的command对象对它的接收者进行操作调用,从而完成操作请求。
这里最关键的思想在于,具体的command用调用者来注册自身,调用者进行回调,在接收者身上执行命令。
command模式示例代码
让我们来看一个简单的例子,它通过command模式实现回调机制。
以fan(风扇)和light(灯)为例。我们的目标是设计一个switch,它可以对任一个对象进行 "开" 和 "关" 的操作。fan和light具有不同的接口,这意味着switch必须独立于接收者接口,或者说,它不清楚接收者的接口。为了解决这一问题,每个switch都需要合适的command作为参数。很明显,连接到light的switch和连接到fan的switch具有不同的command。因此,command类必须是抽象的,或者是个接口。
switch的构造函数被调用时,它以一组合适的command作为参数。command作为switch的私有变量保存下来。
调用flipup()和flipdown()操作时,它们只是简单地让合适的command进行execute()操作。switch对调用execute()后将发生些什么一无所知。
testcommand.java
class fan {
public void startrotate() {
system.out.println("fan is rotating");
}
public void stoprotate() {
system.out.println("fan is not rotating");
}
}
class light {
public void turnon( ) {
system.out.println("light is on ");
}
public void turnoff( ) {
system.out.println("light is off");
}
}
class switch {
private command upcommand, downcommand;
public switch( command up, command down) {
upcommand = up; // concrete command registers itself with the invoker
downcommand = down;
}
void flipup( ) { // invoker calls back concrete command, which executes the command on the receiver
upcommand . execute ( ) ;
}
void flipdown( ) {
downcommand . execute ( );
}
}
class lightoncommand implements command {
private light mylight;
public lightoncommand ( light l) {
mylight = l;
}
public void execute( ) {
mylight . turnon( );
}
}
class lightoffcommand implements command {
private light mylight;
public lightoffcommand ( light l) {
mylight = l;
}
public void execute( ) {
mylight . turnoff( );
}
}
class fanoncommand implements command {
private fan myfan;
public fanoncommand ( fan f) {
myfan = f;
}
public void execute( ) {
myfan . startrotate( );
}
}
class fanoffcommand implements command {
private fan myfan;
public fanoffcommand ( fan f) {
myfan = f;
}
public void execute( ) {
myfan . stoprotate( );
}
}
public class testcommand {
public static void main(string[] args) {
light testlight = new light( );
lightoncommand testloc = new lightoncommand(testlight);
lightoffcommand testlfc = new lightoffcommand(testlight);
switch testswitch = new switch(testloc,testlfc);
testswitch.flipup( );
testswitch.flipdown( );
fan testfan = new fan( );
fanoncommand foc = new fanoncommand(testfan);
fanoffcommand ffc = new fanoffcommand(testfan);
switch ts = new switch( foc,ffc);
ts.flipup( );
ts.flipdown( );
}
}
command.java
public interface command {
public abstract void execute ( );
}
在上面的示例代码中,command模式将 "调用操作的对象" (switch)和 "知道如何执行操作的对象" (light和fan)完全分离开来。这带来了很大的灵活性:发送请求的对象只需要知道如何发送;它不必知道如何完成请求。
command模式实现transaction
command模式也被称为action(动作)模式或transaction(事务)模式。假设有一个服务器,它接收并处理客户通过tcp/ip socket连接发送的transaction。这些transaction包含一个命令,后跟零个或多个 参数。
一些设计者可能会使用switch语句,每个command对应一个case。在一个面向对象工程的设计中,代码中如果使用switch语句,往往表示这是一个糟糕的设计。command模式展现的是支持transaction的面向对象的方法,它可以用于解决这类设计问题。
在testtransactioncommand.java程序的客户代码中,所有请求都被封装在通用的transactioncommand对象中。transactioncommand对象由客户创建并用commandmanager进行登记。等待的请求可以通过调用runcommands()在不同时期被执行,这带来了很大的灵活性。而且我们还可以将多个command组装成一个复合command。示例代码中还有commandargument,commandreceiver,commandmanager这些类,以及transactioncommand的子类--addcommand和subtractcommand。下面是对这些类的介绍。
? commandargument是一个helper类,它保存命令的参数。如果是大量(或可变数量)的任何类型的参数,它可以被重写,以简化参数的传递工作。
? commandreceiver实现所有的命令处理方法(command-processing method),它用singleton模式来实现。
? commandmanager是调用者,和前面例子中的switch相当。它在其私有mycommand变量中保存通用transactioncommand对象。runcommands()被调用时,它调用合适的transactioncommand对象的execute()。
java中,可以根据一个包含类名的字符串查找类的定义。在transactioncommand类的execute ()操作中,我先计算出类名,然后将它链接到运行系统中--也就是说,类是根据需要被即时载入的。这里所采用的命名方式是,在命令名后连接一个 "command" 字符串作为transaction command子类名,这样它就可以被动态载入。
注意,newinstance()返回的class对象必须被转换为合适的类型。这意味着新的类要么必须实现一个接口,要么继承一个在编译期就为程序所知道的现有的类。本例中我们是实现command接口,所以不存在问题。
file://testtransactioncommand.java/
import java.util.*;
final class commandreceiver {
private int[] c;
private commandargument a;
private commandreceiver(){
c = new int[2];
}
private static commandreceiver cr = new commandreceiver();
public static commandreceiver gethandle() {
return cr;
}
public void setcommandargument(commandargument a) {
this.a = a;
}
public void methadd() {
c = a.getarguments();
system.out.println("the result is " + (c[0]+c[1]));
}
public void methsubtract() {
c = a.getarguments();
system.out.println("the result is " + (c[0]-c[1]));
}
}
class commandmanager {
private command mycommand;
public commandmanager(command mycommand) {
this.mycommand = mycommand ;
}
public void runcommands( ) {
mycommand.execute();
}
}
class transactioncommand implements command {
private commandreceiver commandreceiver;
private vector commandnamelist,commandargumentlist;
private string commandname;
private commandargument commandargument;
private command command;
public transactioncommand () {
this(null,null);
}
public transactioncommand ( vector commandnamelist, vector
commandargumentlist){
this.commandnamelist = commandnamelist;
this.commandargumentlist = commandargumentlist;
commandreceiver = commandreceiver.gethandle();
}
public void execute( ) {
for (int i = 0; i < commandnamelist.size(); i++) {
commandname = (string)(commandnamelist.get(i));
commandargument = (commandargument)((commandargumentlist.get(i)));
commandreceiver.setcommandargument(commandargument);
string classname = commandname + "command";
try {
class cls = class.forname(classname);
command = (command) cls.newinstance();
}
catch (throwable e) {
system.err.println(e);
}
command.execute();
}
}
}
class addcommand extends transactioncommand {
private commandreceiver cr;
public addcommand () {
cr = commandreceiver.gethandle();
}
public void execute( ) {
cr.methadd();
}
}
class subtractcommand extends transactioncommand {
private commandreceiver cr;
public subtractcommand () {
cr = commandreceiver.gethandle();
}
public void execute( ) {
cr.methsubtract();
}
}
class commandargument {
private int[] args;
commandargument() {
args = new int[2];
}
public int[] getarguments() {
return args;
}
public void setargument(int i1, int i2) {
args[0] = i1; args[1] = i2;
}
}
public class testtransactioncommand {
private vector clist,alist;
public testtransactioncommand() {
clist = new vector();
alist = new vector();
}
public void clearbuffer(vector c, vector a) {
clist.removeall(c);
alist.removeall(a);
}
public vector getclist() {
return clist;
}
public vector getalist() {
return alist;
}
public static void main(string[] args) {
commandargument ca,ca2;
testtransactioncommand t = new testtransactioncommand();
ca = new commandargument();
ca.setargument(2,8);
vector myclist = t.getclist();
vector myalist = t.getalist();
myclist.addelement("add"); myalist.addelement(ca);
transactioncommand tc = new transactioncommand(myclist,myalist);
commandmanager cm = new commandmanager(tc);
cm.runcommands();
t.clearbuffer(myclist,myalist);
ca2 = new commandargument();
ca2.setargument(5,7);
myclist = t.getclist();
myalist = t.getalist();
myclist.addelement("subtract"); myalist.addelement(ca2);
myclist.addelement("add"); myalist.addelement(ca2);
transactioncommand tc2 = new transactioncommand(myclist,myalist);
commandmanager cm2 = new commandmanager(tc2);
cm2.runcommands();
}
}
命令及其参数保存在列表中,并被封装成通用transactioncommand对象。通用transactioncommand用commandmanager来注册。任何时候,命令可以在commandmanager类中通过调用runcommands()接口来执行。
客户代码不依赖于任何具体的transactioncommand子类,也就是说,我的设计是针对接口而不是实现。这带来了灵活性:要想增加一个新的命令,只需要定义一个新的transactioncommand子类,并在commandreceiver类中提供新的命令处理方法的实现。仅此而已。
结论
command模式具有以下优点:
1. command将 "进行操作请求" 的对象和 "知道如何执行操作" 的对象分离开来(即,解耦)。
2. command是个很棒的对象。它可以象任何其它对象一样被使用和继承。
3. 多个command可以被组装成一个复合command。
4. 很容易增加新的command,因为不需要修改现有的类。
如果执行过的命令序列被保存在一个历史列表中,就可以遍历这个列表来提供undo和redo操作。要想实现这一功能,必须在command接口中有一个unexecute()操作。这一练习留给读者自己去完成。
---------------------------------------------------------------------------
相关资源
? design patterns by gamma, helm, johnson, vlissides, addison-wesley, 1994, isbn 0-201-63361-2 http://www.bookbuyer.com/cgi-bin/gettitle.cgi?isbn=0201633612
? dr. dobb´s journal, january 1998: "java reflection: not just for tool developers," by paul tremblett http://www.ddj.com/articles/1998/9801/9801c/9801c.htm
? sun´s reflection page
http://java.sun.com/docs/books/tutorial/reflect/index.html
? "the java language environment -- a white paper" (may 1996), by james gosling and henry mcgilton covers details about java´s dynamic loading and binding mechanism
http://java.sun.com/docs/white/langenv/
for more on taking advantage of java´s unique feature of dynamic loading and binding mechanism to build a dynamic and dynamically-extensible system, see
http://java.sun.com/docs/white/langenv/
闽公网安备 35060202000074号