設計模式之命令模式
定義
將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化,對請求排隊或記錄請求日誌,以及支援可撤銷的操作。
可以類比現實生活中我們使用電視遙控器開關機,或者去餐廳吃飯向服務員點餐的過程,使用者不需要知道點的菜是具體哪個廚師做的,
廚師也不需要知道這個菜是哪個使用者點的,命令傳送者和執行者之間解耦。
結構
- 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();
}
}
}
客戶端
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原始碼分析)
命令模式(詳解版)
設計模式——命令模式
研磨設計模式-書籍