【1】命令模式

① 定义

命令模式将​​“请求”封装成对象​​,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。命令模式又称为​​行动(Action)模式或交易(Transaction)模式​​。

命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象

每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。也就是说​​“发出请求的对象”​​​和​​“接受与执行这些请求的对象”​​分隔开来。

一个命令对象通过在特定接收者上绑定一组动作来封装一个请求,要达到这一点,​​命令对象将动作和接收者包进对象中​​。这个对象只暴露出一个execute()方法,当此方法被调用的时候,接收者就会进行这些动作。从外面来看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execute()方法,请求的目的就能达到。

定义命令模式类图
认真学习设计模式之命令模式(Command Pattern)_解耦

② 五大对象

​Command(抽象命令类)​​:抽象出命令对象,可以根据不同的命令类型。写出不同的实现类。

​ConcreteCommand(具体命令类)​​:实现了抽象命令对象的具体实现。

​Invoker(调用者/请求者)​​:请求的发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令来之间存在关联。在程序运行时,将调用命令对象的execute() ,间接调用接收者的相关操作。

​Receiver(接收者)​​:接收者执行与请求相关的操作,真正执行命令的对象。具体实现对请求的业务处理。未抽象前,实际执行操作内容的对象。

​Client(客户端)​​:在客户类中需要创建调用者对象,具体命令类对象,在创建具体命令对象时指定对应的接收者。发送者和接收者之间没有直接关系,都通过命令对象来调用。


③ 空命令对象

如果不想每次都检查命令对象是否为null,则可以指定一个默认的对象-NoCommand:

public class NoCommand implements Command {
public void execute(){};
}

NoCommand对象是一个空对象(null object)的例子。当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理null的责任转移给空对象。在许多设计模式中,都会看到空对象的使用。甚至有些时候,空对象本身也被视为是一种设计模式。


④ 宏命令

在宏命令中,用命令数组存储一大堆的命令,当这个宏命令被执行时,就一次性执行数组里的每个命令。

public class MacroCommand implements Command {
Command[] commands;

public MacroCommand(Command[] commands){
this.commands=commands;
}
public void execute(){
for(int i=0;i<commands.length;i++){
commands[i].execute();
}
}
}

宏命令是命令的一种简单的延伸,允许调用多个命令。

为何命令对象不直接实现execute()方法的细节?

也就是接收者一定有必要存在吗?一般来说我们尽量设计“傻瓜”命令对象,它只懂得调用一个接收者的一个行为。然而有许多“聪明”命令对象会实现许多逻辑,直接完成一个请求。当然可以设计聪明的命令对象,只是这样一来,调用者和接收者之间的解耦程度是比不上“傻瓜”命令对象的,而且,你也不能把够把接收者当做参数传给命令。


【2】代码实现实例

① 首先定义一个命令的接收者,也就是到最后真正执行命令的那个人。

public class Receiver {
public void action() {
System.out.println("命令模式的命令被执行了......");
}
}

② 然后定义抽象命令和抽象命令的具体实现,具体命令类中需要持有真正执行命令的那个对象。

public interface Command {
// 调用命令
public void execute();
}

//命令对象:接收者和动作
public class ConcreteCommand implements Command{

private Receiver receiver; //持有真正执行命令对象引用

public ConcreteCommand(Receiver receiver) {
super();
this.receiver = receiver;
}
@Override
public void execute() {
//调用接收者执行命令的方法
receiver.action();
}
}

③ 接下来就可以定义命令的发起者了,发起者需要持有一个命令对象。以便来发起命令。

public class Invoker {
private Command command; //持有命令对象的引用

public Invoker(Command command) {
super();
this.command = command;
}

public void call() {
// 请求者调用命令对象执行命令的那个execute方法
command.execute();
}
}

调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。调用者可以接受命令当做参数,甚至在运行时动态地进行–多态,动态绑定。


④ 客户端

public class Client {
public static void main(String[] args) {
//通过请求者(Invoker)调用命令对象(Command),命令对象中调用命令具体执行者(Receiver)
Command command = new ConcreteCommand(new Receiver());
Invoker invoker = new Invoker(command);
invoker.call();
}
}

代码的UML图如下:
认真学习设计模式之命令模式(Command Pattern)_解耦_02
使用场景

  • Struts2中action中的调用过程中存在命令模式。
  • 数据库中的事务机制的底层实现。
  • 命令的撤销和恢复:增加相应的撤销和恢复命令的方法(比如数据库中的事务回滚)。

命令允许请求的一方和接收请求的一方能够独立演化,从而具有以下的优点:

(1)命令模式使新的命令很容易地被加入到系统里。

(2)允许接收请求的一方决定是否要否决请求。

(3)能较容易地设计一个命令队列。

(4)可以容易地实现对请求的撤销和恢复。

(5)在需要的情况下,可以较容易地将命令记入日志。

【3】命令模式更多用途

① 队列请求

命令模式可以将​​运算块打包(一个接收者和一组动作)​​,然后将它传来传去,就像是一般的对象一样。现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,它甚至可以在不同的线程中被调用。我们可以利用这样的特性衍生一些应用,例如:日程安排(Scheduler)、线程池、工作队列等。

想象有一个工作队列:你在某一端添加命令,然后另一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令。。。

认真学习设计模式之命令模式(Command Pattern)_工作队列_03
请注意,​​​工作队列类和进行计算的对象之间是完全解耦的​​。工作队列不在乎到底做些什么,它们只知道取出命令对象,然后调用其execute()方法。类似地,它们只要是实现命令模式的对象,就可以放入队列,当线程可用时,就调用次对象的execute()方法。


② 日志请求

某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后重新调用这些动作恢复到之前的状态。通过新增两个方法(store()/load()),命令模式就能够支持这一点。在Java中,我们可以利用对象的序列化(Serialization)实现这些方法,但是一般认为序列化最好还是只用在对象的持久化上(persistence)。

我们可以这样实现:当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的execute()方法。

【4】总结

命令模式将发出请求的对象和执行请求的对象解耦。在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。

调用者通过调用命令对象的execute方法发出请求,这会使得接收者的动作被调用。

调用者可以接收命令当做参数,甚至在运行时动态地进行。

命令可以支持撤销,具体做法是实现一个undo方法来回到execute被执行前的状态。

宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销。

实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者。