一、命令模式的定义
将“请求”封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销。
命令接口–ICommand
public interface ICommand {
public void execute();
public void undo();
}
定义统一的接口,所有的命令类都需要实现该接口。
命令类–ConcreteCommand
public class ConcreteCommand implements ICommand {
private Reciever reciever;
public ConcreteCommand(Reciever receiver) {
this.reciever = receiver;
}
public void execute() {
reciever.action();
}
public void undo() {
reciever.undo();
}
}
将某一个具体的“请求”封装成具体的命令类,实现ICommand接口。命令模式的核心目的是将发出请求的对象和执行请求的对象解耦,被解耦的两者之间是通过命令对象进行沟通的,命令对象封装了接受者及其一个或多个动作。比如:开灯命令,可以是LightOnCommand;
命令执行者—Reciever
public class Reciever {
private String recieverName;
pubic Reciever(String name) {
this.recieverName = name;
}
public void action() {
System.out.println("do action...");
}
public void undoAction() {
System.out.println("undo action ...");
}
}
该类是真正执行命令的类,实质上它根本不知道ICommand和Command的存在,所以它只要纯粹完成自己的逻辑。比如一个电灯类(Light),就只需要实现开关电灯的逻辑,并暴露出调用接口就行了。
调用者–Invoker
public class Invoker {
private ICommand command;
public Invoker() {}
public void setCommand(ICommand command) {
this.command = command;
}
// 调用创建时就设置好的命令对象
public invokeCommand() {
this.command.execute();
}
// 动态调用通过参数传过来的命令对象
public invokeCommand(ICommand command) {
command.execute();
}
public invokeUndo() {
this.command.undo();
}
}
命令的调用者,它持有一个或者多个命令对象,在某个时间点调用命令对象的execute方法,将请求付诸执行。
客户端–Client
public class Client {
Receiver reciever = new Reciever();
ICommand c = new ConcreteCommand(reciever);
Invoker invoker = new Invoker();
invoker.setCommand(c);
invoker.invokeCommand();
}
负责创建具体的命令对象,并设置命令的执行者。
宏命令
public class MacroCommand implements ICommand {
ICommands[] commands;
public MacroCommand(ICommand[] commands) {
this.commands = commands;
}
public void execute() {
for (int i=0; i<commands.length; i++) {
commands[i].execute();
}
}
public void undo() {
for (int i=0; i<commands.length; i++) {
commands[i].undo();
}
}
}
宏命令的目的是,制造一种新的命令来执行其他一堆命令。这样这个宏命令也是一个实现了ICommand的命令对象,因此可以普通的命令对象一样被执行。
二、一个具体的例子
实现一个控制电灯开关的遥控器,有三个功能:开、关、撤销操作。
/**
* 命令真正执行者,对应类图中的Reciever
*/
public class Light {
public void onLight() {
System.out.println("light on...");
}
public void offLight() {
System.out.println("light off...");
}
public void undo() {
System.out.println("light undo...")
}
}
/**
* 开灯命令,对应类图中的ConcreteCommand
*/
public class LightOnCommand implements ICommand{
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.onLight();
}
public void undo() {
light.undo();
}
}
/**
* 关灯命令,对应类图中的ConcreteCommand
*/
public class LightOffCommand implements ICommand{
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.offLight();
}
public void undo() {
light.undo();
}
}
/**
* 定义一个空命令对象,它是一个空对象,不做任何事情。可能遥控器在出厂时并没有
* 设置真正有效的命令对象,就可以用空对象代替,以便后续在设置真正的命令对象。
*/
public class NoCommand implements ICommand {
public void execute(){}
public void undo(){}
}
/**
* 命令调用者,对应类图中的Invoker
*/
public class LightController {
// 持有多个命令对象
private ICommand[] commands;
// 记录前一个命令对象,用于“撤销”操作
private ICommand undoCommand;
public LightController() {
// 用于保存开和关两个命令对象
this.commands = new ICommand[2];
// 初始化
ICommand noCommand = new NoCommand();
for(int i=0; i<2; i++){
command[i] = noCommand;
}
undoCommand = noCommand;
}
public void setCommands(ICommand onCommand, ICommand offCommand) {
// 第一个元素被设为开命令对象
commands[0] = onCommand;
commands[1] = offCommand;
}
// 按下打开电灯的按钮(请求打开电灯)
public void onButtonPushed() {
commands[0].execute();
undoCommand = commands[0];
}
// 按下打关电灯的按钮(请求关掉电灯)
public void offButtonPushed() {
commands[1].execute();
undoCommand = commands[1];
}
// 按下撤销按钮,由对应的命令对象执行其撤销操作
pubic void undoButtonPushed() {
undoCommand.undo();
}
// ...其他代码
}
/**
* 创建者,对应类图中的Client类
*/
public class LightControllerTest {
public void static main(String[] args) {
// 创建命令对象及其执行者
Light light = new Light();
LightOnCommand onCommand = new LightOnCommand(light);
LightOffCommand offCommand = new LightOffCommand(light);
LightController lightController = new LightController();
lightController.setCommands(onCommand, offCommand);
// 测试
lightController.onButtonPushed();
lightController.offButtonPushed();
lightController.undoButtonPushed();
}
}
三、优缺点
优点
- 将发出请求的对象与执行请求的对象解耦。
- 调用者可以执行通过参数传过来的命令对象,因而可以在运行时动态执行命令。
- 命令的发送者和命令执行者有不同的生命周期。命令发送了并不是立即执行。
- 可以使用宏命令任意组合一些命令,比如实现“一键式家居控制”的需求,就可以将各种家电的命令对象组合成一组。
- 因为将“请求”封装成了对象,使得命令可以被传递,被延时执行。比如,将命令对象发送到远程服务进行执行;也可以将命令对象扔到队列中,由专门的执行命令线程进行执行,执行命令的线程不需要知道具体命令内容,只要是实现了ICommand接口的对象,它都可以执行。
- 因为将“请求”封装成了对象,从而使得可以对请求的执行过程进行一些控制。比如“撤销”操作可以让命令执行失败之后,进行撤销,可用于事务执行。
- 可将命令记录到日志,系统故障后恢复等。
缺点
- 每一个命令都封装为命令对象,导致类太多。
- 对于这一点,个人感觉不需要如此细的粒度,比如LightOnCommand,LightOffCommand,只要设计一个合适的数据结构,通过数据来表达,就可以省掉很多类,代码的灵活和可扩展性也不受影响。
四、应用场景
队列
命令对象扔到队列中,由专门的执行命令线程进行执行,执行命令的线程不需要知道具体命令内容,只要是实现了ICommand接口的对象,它都可执行。因为命令模式下请求者和执行者可以有不同的生命周期,所以命令对象在创建很久以后,仍然可以被执行。另外,可以使用线程池来执行这些命令,只要这些命令实现Runable接口。
日志
因为命令对象可以对命令自身进行管理,比如可以输出命令日志,所以可以用于“容灾”等需要记录所有命令的应用。在这些应用中,命令对象中新增记录日志将命令存储到磁盘中,一旦系统崩溃或死机,重启后可以重新加载命令,再批次执行命令对象的execute方法。比如数据库事务处理,电子表格等应用。
状态条
如果假如系统需要按顺序执行一系列的命令操作,并且需要了解命令执行进度和状态,则可以让每个命令对象都提供一个获取命令执行状态的方法,系统调用该方法显示状态。
需要多级撤销或重做操作
可以在Invoker中用一个栈来存储执行过的命令对象,一旦需要撤销或重做操作,可以通过pop出最近的一个命令对象,并执行其undo方法。
web服务请求处理
将每个web请求封装成一个命令对象。比如struts框架中,就用到了命令模式: Struts框架中,在模型层都要继承一个Action接口,并实现execute方法,其实这个Action就是命令类。为什么Struts会应用命令模式,是因为Struts的核心控制器ActionServlet只有一个,相当于Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的Command。这样,就需要在ActionServlet和模型层之间解耦,而命令模式正好解决这个问题。