设计模式之命令模式
定义
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
可以类比现实生活中我们使用电视遥控器开关机,或者去餐厅吃饭向服务员点餐的过程,用户不需要知道点的菜是具体哪个厨师做的,
厨师也不需要知道这个菜是哪个用户点的,命令发送者和执行者之间解耦。
结构
- Command,命令接口,定义执行的方法。
- ConcreteCommand,具体命令,拥有接收者对象,调用接收者的功能来完成命令要执行的操作。
- Receiver,接收者,真正执行命令的对象。
- Invoker,调用者,是请求的发送者,通常会拥有很多命令对象,并通过访问命令对象来执行相关请求。
- Client,客户端,也可以称为装配者,组装命令对象和接收者,并触发执行。
以用户餐厅点餐为例,用户就是客户端,服务员就是调用者,点餐就是命令,厨师就是接收者。
简单实现
命令接口
public interface Command {
void execute();
}
具体命令
public class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.action();
}
}
接收者
/**
* 命令接收者
*/
public class Receiver {
public void action() {
System.out.println("Receiver the command and execute");
}
}
调用者
public class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void runCommand() {
command.execute();
}
}
客户端
public class Client {
public static void main(String[] args) {
//组装命令和执行者
Receiver receiver = new Receiver();
Command command = new ConcreteCommand(receiver);
Invoker invoker = new Invoker(command);
invoker.runCommand();
}
}
命令的撤销和恢复
命令模式的关键之处就是将请求封装成对象,也就是命令对象,并定义了统一的执行操作的接口,这个命令对象可以被存储,转发,记录,处理,撤销等,
整个命令模式都是围绕这个对象在进行。这里我们模拟实现一个支持撤销和恢复的简单文本编辑器,类似EditPlus或Word的撤销和恢复功能。
有两种思路来实现这种撤销功能
- 一种是补偿式,又称反操作式,比如被撤销的操作是添加,撤销就是删除。
- 另一种是存储恢复式,将操作前的状态记录下来,撤销的时候直接恢复回去就可以了。关于这种方式,我们学习到备忘录模式时再详解。
这里我们使用第一种方式实现撤销功能。
/**
* 命令接收者
*/
public class Receiver {
/**
* 文本内容
*/
private String textContent = "";
/**
* 文本追加
*/
public void append(String target) {
System.out.println("操作前内容:" + textContent);
textContent = textContent.concat(target);
System.out.println("操作后内容:" + textContent);
}
/**
* 文本删除
*/
public void remove(String target) {
System.out.println("操作前内容:" + textContent);
if (textContent.endsWith(target)) {
textContent = textContent.substring(0, textContent.length() - target.length());
}
System.out.println("操作后内容:" + textContent);
}
}
命令接口
public interface Command {
/**
* 命令执行
*/
void execute();
/**
* 命令撤销
*/
void undo();
}
文本追加命令
public class AppendCommand implements Command {
private Receiver receiver;
private String target;
public AppendCommand(Receiver receiver, String target) {
this.receiver = receiver;
this.target = target;
}
@Override
public void execute() {
receiver.append(target);
}
@Override
public void undo() {
receiver.remove(target);
}
}
文本删除命令
public class RemoveCommand implements Command {
private Receiver receiver;
private String target;
public RemoveCommand(Receiver receiver, String target) {
this.receiver = receiver;
this.target = target;
}
@Override
public void execute() {
receiver.remove(target);
}
@Override
public void undo() {
receiver.append(target);
}
}
文本编辑器(调用者),内部保存命令执行的历史记录(可撤销的列表)和撤销执行的历史记录(可恢复的列表),有撤销才会有恢复,
所以在执行撤销的时候向可恢复列表添加命令。撤销和恢复都是最后执行的要先撤销和恢复,所以使用栈存储。
import java.util.Stack;
/**
* 文本编辑器,支持撤销和恢复
*/
public class TextEditor {
private Command command;
//操作的历史记录
private Stack<Command> undoStack = new Stack<>();
//撤销的历史记录
private Stack<Command> redoStack = new Stack<>();
public void setCommand(Command command) {
this.command = command;
}
public void editText() {
command.execute();
undoStack.push(command);
}
/**
* 撤销功能
*/
public void undoText() {
if (!undoStack.isEmpty()) {
Command command = undoStack.pop();
command.undo();
redoStack.push(command);
}
}
/**
* 恢复功能
*/
public void redoText() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
}
}
}
客户端
public class Client {
public static void main(String[] args) {
//组装命令和执行者
Receiver receiver = new Receiver();
TextEditor textEditor = new TextEditor();
//追加hello
textEditor.setCommand(new AppendCommand(receiver, "hello"));
textEditor.editText();
//追加world
textEditor.setCommand(new AppendCommand(receiver, "world"));
textEditor.editText();
//删除orld
textEditor.setCommand(new RemoveCommand(receiver, "orld"));
textEditor.editText();
//撤销
textEditor.undoText();
//撤销
textEditor.undoText();
//恢复
textEditor.redoText();
}
}
输出为
操作前内容:
操作后内容:hello
操作前内容:hello
操作后内容:helloworld
操作前内容:helloworld
操作后内容:hellow
操作前内容:hellow
操作后内容:helloworld
操作前内容:helloworld
操作后内容:hello
操作前内容:hello
操作后内容:helloworld
结果符合预期
宏命令
简单来说就是包含多个命令的命令,在餐厅点餐中,用户所有点的菜组成的菜单就是一个宏命令,每一道菜都是一个命令,类似于组合模式,这里就不实现了。
简化的命令模式
在实际开发中,我们可以简化命令模式,将具体命令和接收者合二为一,调用者也不需要持有命令对象了,直接通过方法参数传递过来,
将具体命令类的实现改成匿名内部类实现,这个时候的命令模式基本上等同于java回调机制的实现。
public interface Command {
void execute();
}
/**
* 调用者
*/
public class Invoker {
public void runCommand(Command command) {
command.execute();
}
}
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
invoker.runCommand(() -> {
System.out.println("Concrete Command execute");
});
}
}
命令模式在JDK的实现
jdk中线程池ThreadPoolExecutor的实现
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Client {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
executor.execute(() -> {
System.out.println("test execute");
});
executor.shutdown();
}
}
ThreadPoolExecutor可以看做Invoker调用者,Runnable就是Command接口,将来不及执行的请求放到队列中,这可以看做命令模式中的请求队列化操作。
总结
优点
- 更松散的耦合,命令的发起者和命令的执行者相互之间不需要知道对方。
- 更动态的控制,可以动态的对命令对象进行队列化,日志化,撤销等操作。
- 很容易组成复合命令,也就是宏命令,使系统操作更简单,功能更强大。
- 更好的扩展性,很容易增加新的命令对象。
缺点
- 可能会导致创建过多的具体命令类。
本质
命令模式的本质是封装请求,封装为请求就可以进行撤销,队列化,宏命令等处理了。
使用场景
- 如果需要在不同的时刻排队执行请求,可以使用命令模式,将请求封装成命令对象并队列化。
- 如果需要支持撤销操作。
参考
大战设计模式【8】—— 命令模式
设计模式的征途—19.命令(Command)模式
设计模式(十五)——命令模式(Spring框架的JdbcTemplate源码分析)
命令模式(详解版)
设计模式——命令模式
研磨设计模式-书籍