命令模式(command pattern)

本文深入解析命令模式的概念及其应用场景,展示了如何通过命令模式解耦调用者与接收者,实现命令对象的动态构建与组合,同时提供了实现撤销(undo)功能的具体示例。
命令模式,实现了把命令调用与命令执行分开。用途广泛。

它可以批量执行命令,比如它可以用于进度条的显示。多个命令顺序执行,进度
条来表示命令的执行进度。如果每个命令都用command封装,成为命令对象
(command object),并且每个命令对象都有获取执行时间的方法,那么就可以方
便的获取整个执行过程的时间。据此就可以用进度条显示出这一系列任务的执行
进度。它可以实现重做(undo)操作。数据库事物中的rollback处理。GUI程序使用
命令模式更为常见,从中,你甚至可以看到MVC架构的缩影。

命令模式的用法多种多样,但总离不开三个角色和一个对象: 调用者、接受者、
客户端,和命令对象(command object). 调用者决定命令执行的时机,即它是命
令的发起者;它不知道命令的执行细节,只管发起。接受者是真正执行命令的对
象。命令对象是实现命令接口的对象,命令对象往往封装或引用了接收者。实际
编程中也不必拘泥于形式,命令对象和接收者也可合二为一,即这个对象既实现
了命令接口,又亲自执行了命令细节。客户端负责初始化调用者、接收者、命令
对象以及提供必要的信息(参数),一旦初始化完毕,各个对象的联系通路也就
建立起来了。

下面,我举一个经典的例子来说明调用者、接收者、客户端和命令对象的关系。
这个例子中要调用两个命令,我还会在后面给出调用一个命令的情况。

(代码来自维基)

/*the Invoker class*/
public class Switch {

private Command flipUpCommand;
private Command flipDownCommand;

public Switch(Command flipUpCmd, Command flipDownCmd) {
this.flipUpCommand = flipUpCmd;
this.flipDownCommand = flipDownCmd;
}

public void flipUp() {
flipUpCommand.execute();
}

public void flipDown() {
flipDownCommand.execute();
}
}

/*Receiver class*/
public class Light {

public Light() { }

public void turnOn() {
System.out.println("The light is on");
}

public void turnOff() {
System.out.println("The light is off");
}
}

/*the Command interface*/
public interface Command {
void execute();
}

/*the Command for turning on the light*/
public class FlipUpCommand implements Command {

private Light theLight;

public FlipUpCommand(Light light) {
this.theLight=light;
}

public void execute(){
theLight.turnOn();
}
}

/*the Command for turning off the light*/
public class FlipDownCommand implements Command {

private Light theLight;

public FlipDownCommand(Light light) {
this.theLight=light;
}

public void execute() {
theLight.turnOff();
}
}

/*The test class or client*/
public class PressSwitch {

public static void main(String[] args) {
Light lamp = new Light();
Command switchUp = new FlipUpCommand(lamp);
Command switchDown = new FlipDownCommand(lamp);

Switch s = new Switch(switchUp,switchDown);

try {
if (args[0].equalsIgnoreCase("ON")) {
s.flipUp();
} else if (args[0].equalsIgnoreCase("OFF")) {
s.flipDown();
} else {
System.out.println("Argument \"ON\" or \"OFF\" is required.");
}
} catch (Exception e){
System.out.println("Arguments required.");
}
}
}


这个例子中的调用者、接收者、客户端、命令对象由注释标明了。两个命令对象
封装、引用了同一个接收者--灯泡。调用者的视线被屏蔽了,调用者只知道开和
关,并不知道开、关的是灯还是别的什么东西。它也不关心具体细节。命令对象
如果去封装了门,那么开、关的操作对象就成了门,而不是灯。这种改变,调用
者也不知道和不关心。

"不关心"是设计模式分析中常常提的一个词。“不关心”是不需要关心,不需要
关心是因为不属于自己负责,不属于自己负责是因为安排,安排是基于分工,分
工提高了效率。而设计模式以及由此发展出的架构从技术上促成了分工。正是分
工意图,导致“不关心”词汇在设计模式中频现。如果开发者只有一人,没有
“不关心”,只能“全关心”。所以,思考设计模式时,有时要假想是好几个人
在开发,要一会儿扮作张三负责这一块,一会儿扮作李四负责那一块儿。这样更
容易理解模式。定义好接口,你做你的,我做我的,互不打扰。

在这个例子中,调用者和接收者被解耦了。

跟解耦相反的情况是调用者和接收者混在一块。那会是这番景象。

if (args[0].equalsIgnoreCase("ON")) {
lamp.turnOn();
}
if (args[0].equalsIgnoreCase("OFF")) {
lamp.turnOff();
}


这里的代码量倒是少了不少,但调用者和接收者是混在一块的。如果没有别的需
求,其实这样做没什么不好。毕竟在可读性不受影响情况下代码少一些不是坏事。
但如果需求不明确指定是开灯、还是开门,或者现在需求中虽然是开门,但以后
可能是开灯,那么混在一起的做法就不好了,一旦需求有变,调用者和接收者都
要修改代码。而分离开的好处,则起码调用者不用修改代码。如果调用者的编写
者是一个人,而接收者的编写者是另一个人,这样的改动就不会让编写调用者的
程序员受到牵连。

在上面的例子中,调用者是指这句话
Switch s = new Switch(switchUp,switchDown);

而这句话之前的初始化工作可以放到一个厂中完成或有容器自动完成。可以让它
们跟调用者不挤在一起,这样真正的把调用者和接收者隔离。

隔离的方式是用了一个中间对象叫做command object来完成。实际上,这种模式
就是MVC的雏形。web程序员都熟悉struts开源框架。该框架是典型的MVC结构,其
中的action一层,就相当于command object这一层。在web中的一个页面请求的流
转过程是这样的:

user(request)-->action-->module

这其实就是调用者-->命令对象-->接收者。

在Web开发中,用户的增删改是常见的功能。在struts框架为基础的系统中,开发
这种功能一般有两种方式,一种是在一个action中做判断,如果添加则调用添加
模块,删除则调用删除模块,这样这个action中就会有if判断语句。另一种方法
是一种操作用一个action完成,添加用AddAction,删除用DeleteAction等。推荐
后者。职责单一,代码不容易变的乱。一般来说,面向对象设计中出现很多
switch statement往往是不好的设计的表现。

command对象不仅仅是简单的封装一下接收者,就像action控制层不仅仅只是转发
一下请求一样,它还可以做点别的事。比如command模式常见的用途----实现
undo.

我们看一个用命令模式实现undo功能的代码.

代码来自:
http://twit88.com/blog/2008/01/26/design-pattern-in-java-101-command-pattern-behavioral-pattern/

[quote]//先定义一个命令接口
public interface Command {
public void execute();
public void unExecute();
}

//定义一个计算器
public class Calculator {
private int current = 0;

public void operation(char operator, int operand) {
switch (operator) {
case '+':
current += operand;
break;
case '-':
current -= operand;
break;
case '*':
current *= operand;
break;
case '/':
current /= operand;
break;
}
System.out.println("Current value = "
+ current + " after " + operator + " " + operand);
}
}

//用这个命令类封装计算器
public class CalculatorCommand implements Command {

private char operator;
private int operand;
private Calculator calculator;

public CalculatorCommand(Calculator calculator,
char operator, int operand) {
this.calculator = calculator;
this.operator = operator;
this.operand = operand;
}

public char getOperator() {
return operator;
}

public void setOperator(char operator) {
this.operator = operator;
}

public int getOperand() {
return operand;
}

public void setOperand(int operand) {
this.operand = operand;
}

public void execute() {
calculator.operation(operator, operand);
}

public void unExecute() {
calculator.operation(undo(operator), operand);
}

// Private helper function
private char undo(char operator) {
char undo;
switch (operator) {
case '+':
undo = '-';
break;
case '-':
undo = '+';
break;
case '*':
undo = '/';
break;
case '/':
undo = '*';
break;
default:
undo = ' ';
break;
}
return undo;
}
}

//用户调用计算器
import java.util.ArrayList;

public class User {
private Calculator calculator = new Calculator();
private ArrayList<Command> commands =
new ArrayList<Command>();

private int current = 0;

public void redo(int levels) {
System.out.println("\n---- Redo " + levels + " levels ");
// Perform redo operations
for (int i = 0; i < levels; i++) {
if (current < commands.size()) {
Command command = commands.get(current++);
command.execute();
}
}
}

public void undo(int levels) {
System.out.println("\n---- Undo " + levels + " levels ");
// Perform undo operations
for (int i = 0; i < levels; i++) {
if (current >= 0) {
Command command = commands.get(--current);
command.unExecute();
}
}
}

public void compute(char operator, int operand) {
// Create command operation and execute it
Command command = new CalculatorCommand(
calculator, operator, operand);
command.execute();

// Add command to undo list
commands.add(command);
current++;
}
}

//客户端
public class TestCommandPattern {

public static void main(String[] args) {
// Create user and let her compute
User user = new User();

user.compute('+', 100);
user.compute('-', 50);
user.compute('*', 10);
user.compute('/', 2);

// Undo 4 commands
user.undo(4);

// Redo 3 commands
user.redo(3);

}
}

[/quote]

在这个例子中,CalculatorCommand对象不仅仅是把请求传递给Calculator,而是
在把请求传递给Calculator之前对请求进行加工、转置,从而实现了undo功能。
undo并不是在接收者这一层实现的,而是在命令对象中实现的。命令对象实现
undo时只是对请求进行转置,最后仍旧调用的接收者,而不是说在命令对象里面
又单独写了一整套的undo逻辑,它仅仅是对请求的处理。对应到struts中action
的编写,如果action中写了过多的业务逻辑,那就是不合适的,action是控制层,
更适合于写对请求进行处理的控制逻辑。MVC与命令模式何其像哉。

此例中user是调用者,user发出请求,请求传递给command对象,command对象再
调用 Calculator. 调用者用一个List来保存执行过的命令(就是压栈命令对象),
当需要undo时,就让命令对象出栈。然后执行命令对象的undo功能。在此过程中,
Calculator永远保持独立,并没有因为需要增加undo功能而更改自身。

数据库的rollback功能和undo功能类似,实现也类似。用类似的方法可以实现
rollback的功能。当然,专业的数据库里的rollback功能的实现可不是这么简单。

下面我再给一个例子。这个例子适用于服务端接收客户端的命令和相应参数然后
执行命令的情况。这个例子非常出色,它很好的表现出了命令模式的精髓。

代码来自:http://www.javaworld.com/javaworld/javatips/jw-javatip68.html?page=4

package pattern;

//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]));
}
}

interface Command{
public void execute();
}

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 = "pattern."+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();
}
}


这个例子中有如下几个对象,

1. 接收者:CommandReceiver
2. 命令接口:Command
3. 命令对象:
1) TransactionCommand 批处理命令对象
2) AddCommand 加法命令
3) SubtractCommand 减法命令
4. CommandManager调用者
5. CommandArgument参数对象,它仅仅是个辅助类,封装参数。
6. TestTransactionCommand 客户端

该程序在客户端初始化完毕各个对象(包括封装命令、封装参数、把命令加进命令
列表等),然后只需要调用CommandManager.runCommand()命令即可。

不用命令模式,按照通常的做法,服务器端接收到命令后,会对命令类型做判断,
形式如下:

服务器端接收用户命令;
If(命令是加法) 则做加法运算;
If(命令是减法) 则做减法运算;

而现在的形式是:

服务器端接收用户命令;
执行用户命令;

这样,服务器端根本就不知道是什么命令(一般而言,如果用了if a is b,就看
做这段程序“知道”,比如if a is 小明, 意味着这段程序知道小明这个人),
而只需要执行命令,而命令的执行细节被曲折迂回的转到了接收者那里。

这个例子中的另一大特色是有两种类型的命令对象,一种是批处理命令对象,一
种是单个的命令对象。批处理命令对象接收请求序列,然后根据请求序列动态的
装配单个命令对象,接着调用该命令对象的执行命令。没有频繁的if if,整个过
程一起哈成。代码显得非常流畅。想想这样一种命令序列如果用If实现,将是怎
样的景象吧(循环中嵌入了许多if判断,这不正是很多人熟悉、厌烦的代码组织方
式吗)。

这种动态组装命令对象方式在网络应用开发中非常有用,比如一台电脑上的图形
变化,另一台电脑上的同一同一图形也跟着同样变化,就可以用这种方式实现。
网络游戏中别人控制的物体在你的电脑上的运动显示就是最为常见的一种可使用
命令模式实现的情景。

概括的说,命令模式起码有4个优点,

1. 解耦了调用者和接收者。
2. 命令对象可以动态构建,可以被当做参数传来传去。
3. 很多命令可以被组合成一个大命令(比如例子中的TransactionCommand)。
4. 因为不需要更改已有的类(指接收者),所以方便添加新命令(然后引用已有类即可)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值