设计模式 - D6 - 命令模式
封装调用
封装调用时指把运算块包装成形,而调用此运算的对象不需要关心事情是如何进行,只需如何使用包装成形的方法来完成即可。利用封装方法调用,可以完成一些记录日志等操作。因此,在此引入命令模式
命令模式
可将“发出请求的对象”从“接收与执行这些请求的对象”解耦定义
定义
命令模式:将请求封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作
- 一个命令对象通过在特定接收者上绑定一组动作来封装一个请求,即将动作和接收者包进对象中;同时,命令对象只暴露出一个execute()方法,此方法被调用时,接收者就会进行这些操作,但对外来说,调用者不知道进行哪些操作,从而实现将“发出请求的对象”从“接收与执行这些请求的对象”解耦
- Client:负责创建一个ConcreteCommand,并设置其包含的Receiver
- Command:为所有命令声明了一个接口,调用Command对象的execute()方法就可以让Receiver进行相关的动作,undo()方法
- ConcreteCommand:定义了动作和接受者之间的绑定关系,Invoker只要调用execute()发出请求,ConcreteCommand就会调用Revicer的一个或多个动作
- Invoker:持有一个命令对象,在某个时刻调用命令的execute()方法
- Receiver:执行其一个或多个动作,任何类都可以作为Receiver
基本命令模式
假设我们现在要使用一个遥控器,在多个开关上控制多种电器进行不同的操作,那么这里遥控器的多个开关就相当于多个Invoker,多种电器就是多种Receiver,而绑定在开关上的操作就是Command
命令(Command)建立
命令接口:
要实现一个命令对象,就必须先实现包含一个方法的命令接口,让所有命令对象实现相同的接口
public interface Command {
void execute();
}
具体命令:
假设要实现一个打开电灯的命令,而一个电灯类(Light)中有两个方法:on()和off(),则一个具体命令可以设计为如下
(电灯类其实就是一个接收者(Receiver))
public class LightOnCommand implements Command{
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
public class Light {
String name;
public Light(String name) {
this.name = name;
}
public void on() {
System.out.println(name + " On");
}
public void off() {
System.out.println(name + " On");
}
}
调用者(Invoker)建立
在这里,调用者就是遥控器,其实现可以是
public class SimpleRemoteControl {
Command command;
public void setCommand(Command command) {
this.command = command;
}
public void buttonWasPressed() {
command.execute();
}
}
由上述代码可见,当我们需要更换命令操作时,无须对调用者的代码进行任何修改,只需创建新的命令对象,然后传入调用者即可
客户建立
在这里,客户用来测试调用者类的功能,它需要创建命令对象(LightOnCommand)、接收者(Light)和调用者(SimpleRemoteControl),然后分别向LightOnCommand和SimpleRemoteControl传入Light和LightOnCommand
public class RemoteControlTest {
public static void main(String[] args) {
// 创建invoker、receiver、command
SimpleRemoteControl invoker = new SimpleRemoteControl();
Light receiver = new Light("Light");
LightOnCommand command = new LightOnCommand(receiver);
// 向invoker传入command
invoker.setCommand(command);
// invoker.buttonWasPressed() -> command.execute() -> light.on()
invoker.buttonWasPressed();
}
}
注:空对象
当我们不想返回一个有意义的对象时,可以使用空对象,从而将处理null的责任转移给空对象。例如,当我们不想使用if (command == null)判断是否存在指令时,可以使用一个实现execute()方法为空方法的NoCommand对象代替
public class NoCommand implements Command {
@Override
public void execute() {}
}
支持撤销的命令模式
命令(Command)
当命令支持撤销时,命令对象就必须加上撤销功能,也就是命令接口要同时提供execute()方法和undo()方法,不论execute()方法执行了什么操作,undo()方法都会进行相反的操作
public interface Command {
void execute();
void undo();
}
public class LightOnCommand implements Command{
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
调用者(Invoker)
为了让调用者能追踪最后被调用命令,可以添加一个实例变量来记录最后被调用命令,然后当需要执行撤销指令时,就通过这个实例变量来调用其undo()方法(此处Invoker扩展为可以使用多个Command)
显然,如果希望undo可以回溯多个操作,则可以使用栈来记录过去所调用的命令
public class SimpleRemoteControl {
private Command[] commands;
private Command undoCommand;
public SimpleRemoteControl() {
commands = new Command[6];
for(int i = 0; i < 6; i++) {
commands[i] = new NoCommand();
}
undoCommand = new NoCommand();
}
public void setCommand(int slot, Command command) {
this.commands[slot] = command;
}
public void buttonWasPressed(int slot) {
commands[slot].execute();
undoCommand = commands[slot];
}
public void undoButtonWasPressed() {
undoCommand.undo();
}
}
客户(Client)
因此,客户就可以修改为
public class RemoteControlTest {
public static void main(String[] args) {
// 创建invoker、receiver、command
SimpleRemoteControl invoker = new SimpleRemoteControl();
Light receiver = new Light("Light");
LightOnCommand command = new LightOnCommand(receiver);
// 向invoker传入command
invoker.setCommand(1, command);
// invoker.buttonWasPressed() -> command.execute() -> light.on()
invoker.buttonWasPressed(1);
invoker.undoButtonWasPressed();
}
}
输出:
Light On
Light Off
支持宏命令的命令模式
当我们想要通过一个命令对象执行一组其他命令时,我们就可以创建一个宏命令模式。宏命令对象,其实就是带有一个命令对象数组/集合的、同时也实现了Command接口的命令对象
public class MacroCommand implements Command{
Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for(int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for(int i = 0; i < commands.length; i++) {
commands[i].undo();
}
}
}
public class RemoteControlTest {
public static void main(String[] args) {
// 创建invoker、receiver、command
SimpleRemoteControl invoker = new SimpleRemoteControl();
Light receiver = new Light("Light");
LightOnCommand command = new LightOnCommand(receiver);
Light receiver1 = new Light("Light1");
LightOnCommand command1 = new LightOnCommand(receiver1);
MacroCommand macroCommand = new MacroCommand(new Command[]{command, command1});
// 向invoker传入command
invoker.setCommand(0, macroCommand);
// invoker.buttonWasPressed() -> command.execute() -> light.on()
invoker.buttonWasPressed(0);
invoker.undoButtonWasPressed();
}
}
输出:
Light On
Light1 On
Light Off
Light1 Off
命令模式的用途
队列请求
命令可以将运算块打包(一个接收者和一组动作);此时,若有一个队列,我们可以在队列的一段添加队列,而队列另一端的线程可以从队列中取出一个命令,调用其execute()方法方法,当该方法调用完成时,就将该命令丢弃,再取出下一个命令
此时,工作队列类和进行计算的对象之间完全时解耦的,即队列此时可能在进行财务运算,下一时刻可能就在读取网络数据,它无须知道执行的具体操作是什么,只需知道执行命令的execute()方法即可
日志请求
通过Java的序列化(Serialization)在请求对象中添加store()和load()方法即可在执行命令时,将历史记录存储在磁盘中。当系统宕机时,就可以将命令对象重新加载,并成批地、依次地调用这些对象的execute()方法