【1】命令模式
① 定义
命令模式将“请求”封装成对象
,以便使用不同的请求、队列或者日志来参数化其他对象
命令模式也支持可撤销的操作。命令模式又称为行动(Action)模式或交易(Transaction)模式
。
命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同
每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。也就是说“发出请求的对象”和“接受与执行这些请求的对象”分隔开来。
一个命令对象通过在特定接收者上绑定一组动作来封装一个请求,要达到这一点,命令对象将动作和接收者包进对象中。这个对象只暴露出一个execute()方法,当此方法被调用的时候,接收者就会进行这些动作。从外面来看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execute()方法,请求的目的就能达到。
定义命令模式类图
② 五大对象
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图如下:
使用场景
- Struts2中action中的调用过程中存在命令模式。
- 数据库中的事务机制的底层实现。
- 命令的撤销和恢复:增加相应的撤销和恢复命令的方法(比如数据库中的事务回滚)。
命令允许请求的一方和接收请求的一方能够独立演化,从而具有以下的优点:
(1)命令模式使新的命令很容易地被加入到系统里。
(2)允许接收请求的一方决定是否要否决请求。
(3)能较容易地设计一个命令队列。
(4)可以容易地实现对请求的撤销和恢复。
(5)在需要的情况下,可以较容易地将命令记入日志。
【3】命令模式更多用途
① 队列请求
命令模式可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,它甚至可以在不同的线程中被调用。我们可以利用这样的特性衍生一些应用,例如:日程安排(Scheduler)、线程池、工作队列等。
想象有一个工作队列:你在某一端添加命令,然后另一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令。。。
请注意,工作队列类和进行计算的对象之间是完全解耦的
。工作队列不在乎到底做些什么,它们只知道取出命令对象,然后调用其execute()方法。类似地,它们只要是实现命令模式的对象,就可以放入队列,当线程可用时,就调用次对象的execute()方法。
② 日志请求
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后重新调用这些动作恢复到之前的状态。通过新增两个方法(store()/load()),命令模式就能够支持这一点。在Java中,我们可以利用对象的序列化(Serialization)实现这些方法,但是一般认为序列化最好还是只用在对象的持久化上(persistence)。
我们可以这样实现:当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的execute()方法。
【4】总结
命令模式将发出请求的对象和执行请求的对象解耦。在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。
调用者通过调用命令对象的execute方法发出请求,这会使得接收者的动作被调用。
调用者可以接收命令当做参数,甚至在运行时动态地进行。
命令可以支持撤销,具体做法是实现一个undo方法来回到execute被执行前的状态。
宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销。
实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者。