意圖
指令模式是一種行為設計模式, 它可将請求轉換為一個包含與請求相關的所有資訊的獨立對象。 該轉換讓你能根據不同的請求将方法參數化、 延遲請求執行或将其放入隊列中, 且能實作可撤銷操作。

問題
假如你正在開發一款新的文字編輯器, 目前的任務是建立一個包含多個按鈕的工具欄, 并讓每個按鈕對應編輯器的不同操作。 你建立了一個非常簡潔的
按鈕
類, 它不僅可用于生成工具欄上的按鈕, 還可用于生成各種對話框的通用按鈕。
應用中的所有按鈕都可以繼承相同的類
盡管所有按鈕看上去都很相似, 但它們可以完成不同的操作 (打開、 儲存、 列印和應用等)。 你會在哪裡放置這些按鈕的點選處理代碼呢? 最簡單的解決方案是在使用按鈕的每個地方都建立大量的子類。 這些子類中包含按鈕點選後必須執行的代碼。
大量的按鈕子類。 沒關系的。
你很快就意識到這種方式有嚴重缺陷。 首先, 你建立了大量的子類, 當每次修改基類
按鈕
時, 你都有可能需要修改所有子類的代碼。 簡單來說, GUI 代碼以一種拙劣的方式依賴于業務邏輯中的不穩定代碼。
多個類實作同一功能。
還有一個部分最難辦。 複制/粘貼文字等操作可能會在多個地方被調用。 例如使用者可以點選工具欄上小小的 “複制” 按鈕, 或者通過上下文菜單複制一些内容, 又或者直接使用鍵盤上的
Ctrl+C
。
我們的程式最初隻有工具欄, 是以可以使用按鈕子類來實作各種不同操作。 換句話來說,
複制按鈕
CopyButton子類包含複制文字的代碼是可行的。 在實作了上下文菜單、 快捷方式和其他功能後, 你要麼需要将操作代碼複制進許多個類中, 要麼需要讓菜單依賴于按鈕, 而後者是更糟糕的選擇。
解決方案
優秀的軟體設計通常會将關注點進行分離, 而這往往會導緻軟體的分層。 最常見的例子: 一層負責使用者圖像界面; 另一層負責業務邏輯。 GUI 層負責在螢幕上渲染美觀的圖形, 捕獲所有輸入并顯示使用者和程式工作的結果。 當需要完成一些重要内容時 (比如計算月球軌道或撰寫年度報告), GUI 層則會将工作委派給業務邏輯底層。
這在代碼中看上去就像這樣: 一個 GUI 對象傳遞一些參數來調用一個業務邏輯對象。 這個過程通常被描述為一個對象發送請求給另一個對象。
GUI 層可以直接通路業務邏輯層。
指令模式建議 GUI 對象不直接送出這些請求。 你應該将請求的所有細節 (例如調用的對象、 方法名稱和參數清單) 抽取出來組成指令類, 該類中僅包含一個用于觸發請求的方法。
指令對象負責連接配接不同的 GUI 和業務邏輯對象。 此後, GUI 對象無需了解業務邏輯對象是否獲得了請求, 也無需了解其對請求進行處理的方式。 GUI 對象觸發指令即可, 指令對象會自行處理所有細節工作。
通過指令通路業務邏輯層。
下一步是讓所有指令實作相同的接口。 該接口通常隻有一個沒有任何參數的執行方法, 讓你能在不和具體指令類耦合的情況下使用同一請求發送者執行不同指令。 此外還有額外的好處, 現在你能在運作時切換連接配接至發送者的指令對象, 以此改變發送者的行為。
你可能會注意到遺漏的一塊拼圖——請求的參數。 GUI 對象可以給業務層對象提供一些參數。 但執行指令方法沒有任何參數, 是以我們如何将請求的詳情發送給接收者呢? 答案是: 使用資料對指令進行預先配置, 或者讓其能夠自行擷取資料。
GUI 對象将指令委派給指令對象。
讓我們回到文本編輯器。 應用指令模式後, 我們不再需要任何按鈕子類來實作點選行為。 我們隻需在
按鈕
Button基類中添加一個成員變量來存儲對于指令對象的引用, 并在點選後執行該指令即可。
你需要為每個可能的操作實作一系列指令類, 并且根據按鈕所需行為将指令和按鈕連接配接起來。
其他菜單、 快捷方式或整個對話框等 GUI 元素都可以通過相同方式來實作。 當使用者與 GUI 元素互動時, 與其連接配接的指令将會被執行。 現在你很可能已經猜到了, 與相同操作相關的元素将會被連接配接到相同的指令, 進而避免了重複代碼。
最後, 指令成為了減少 GUI 和業務邏輯層之間耦合的中間層。 而這僅僅是指令模式所提供的一小部分好處!
真實世界類比
在餐廳裡點餐。
在市中心逛了很久的街後, 你找到了一家不錯的餐廳, 坐在了臨窗的座位上。 一名友善的服務員走近你, 迅速記下你點的食物, 寫在一張紙上。 服務員來到廚房, 把訂單貼在牆上。 過了一段時間, 廚師拿到了訂單, 他根據訂單來準備食物。 廚師将做好的食物和訂單一起放在托盤上。 服務員看到托盤後對訂單進行檢查, 確定所有食物都是你要的, 然後将食物放到了你的桌上。
那張紙就是一個指令, 它在廚師開始烹饪前一直位于隊列中。 指令中包含與烹饪這些食物相關的所有資訊。 廚師能夠根據它馬上開始烹饪, 而無需跑來直接和你确認訂單詳情。
指令模式結構
- 發送者 (Sender)——亦稱 “觸發者 (Invoker)”——類負責對請求進行初始化, 其中必須包含一個成員變量來存儲對于指令對象的引用。 發送者觸發指令, 而不向接收者直接發送請求。 注意, 發送者并不負責建立指令對象: 它通常會通過構造函數從用戶端處獲得預先生成的指令。
- 指令 (Command) 接口通常僅聲明一個執行指令的方法。
-
具體指令 (Concrete Commands) 會實作各種類型的請求。 具體指令自身并不完成工作, 而是會将調用委派給一個業務邏輯對象。 但為了簡化代碼, 這些類可以進行合并。
接收對象執行方法所需的參數可以聲明為具體指令的成員變量。 你可以将指令對象設為不可變, 僅允許通過構造函數對這些成員變量進行初始化。
- 接收者 (Receiver) 類包含部分業務邏輯。 幾乎任何對象都可以作為接收者。 絕大部分指令隻處理如何将請求傳遞到接收者的細節, 接收者自己會完成實際的工作。
- 用戶端 (Client) 會建立并配置具體指令對象。 用戶端必須将包括接收者實體在内的所有請求參數傳遞給指令的構造函數。 此後, 生成的指令就可以與一個或多個發送者相關聯了。
指令模式适用性
- 如果你需要通過操作來參數化對象, 可使用指令模式。
- 指令模式可将特定的方法調用轉化為獨立對象。 這一改變也帶來了許多有趣的應用: 你可以将指令作為方法的參數進行傳遞、 将指令儲存在其他對象中, 或者在運作時切換已連接配接的指令等。
- 舉個例子: 你正在開發一個 GUI 元件 (例如上下文菜單), 你希望使用者能夠配置菜單項, 并在點選菜單項時觸發操作。
- 如果你想要将操作放入隊列中、 操作的執行或者遠端執行操作, 可使用指令模式。
- 同其他對象一樣, 指令也可以實作序列化 (序列化的意思是轉化為字元串), 進而能友善地寫入檔案或資料庫中。 一段時間後, 該字元串可被恢複成為最初的指令對象。 是以, 你可以延遲或計劃指令的執行。 但其功能遠不止如此! 使用同樣的方式, 你還可以将指令放入隊列、 記錄指令或者通過網絡發送指令。
- 如果你想要實作操作復原功能, 可使用指令模式。
- 盡管有很多方法可以實作撤銷和恢複功能, 但指令模式可能是其中最常用的一種。
- 為了能夠復原操作, 你需要實作已執行操作的曆史記錄功能。 指令曆史記錄是一種包含所有已執行指令對象及其相關程式狀态備份的棧結構。
- 這種方法有兩個缺點。 首先, 程式狀态的儲存功能并不容易實作, 因為部分狀态可能是私有的。 你可以使用備忘錄模式來在一定程度上解決這個問題。
- 其次, 備份狀态可能會占用大量記憶體。 是以, 有時你需要借助另一種實作方式: 指令無需恢複原始狀态, 而是執行反向操作。 反向操作也有代價: 它可能會很難甚至是無法實作。
實作方式
- 聲明僅有一個執行方法的指令接口。
- 抽取請求并使之成為實作指令接口的具體指令類。 每個類都必須有一組成員變量來儲存請求參數和對于實際接收者對象的引用。 所有這些變量的數值都必須通過指令構造函數進行初始化。
- 找到擔任發送者職責的類。 在這些類中添加儲存指令的成員變量。 發送者隻能通過指令接口與其指令進行互動。 發送者自身通常并不建立指令對象, 而是通過用戶端代碼擷取。
- 修改發送者使其執行指令, 而非直接将請求發送給接收者。
- 用戶端必須按照以下順序來初始化對象:
- 建立接收者。
- 建立指令, 如有需要可将其關聯至接收者。
- 建立發送者并将其與特定指令關聯。
指令模式優缺點
- 單一職責原則。 你可以解耦觸發和執行操作的類。
- 開閉原則。 你可以在不修改已有用戶端代碼的情況下在程式中建立新的指令。
- 你可以實作撤銷和恢複功能。
- 你可以實作操作的延遲執行。
- 你可以将一組簡單指令組合成一個複雜指令。
- 代碼可能會變得更加複雜, 因為你在發送者和接收者之間增加了一個全新的層次。
與其他模式的關系
- 責任鍊模式、 指令模式、 中介者模式和觀察者模式用于處理請求發送者和接收者之間的不同連接配接方式:
- 責任鍊按照順序将請求動态傳遞給一系列的潛在接收者, 直至其中一名接收者對請求進行處理。
- 指令在發送者和請求者之間建立單向連接配接。
- 中介者清除了發送者和請求者之間的直接連接配接, 強制它們通過一個中介對象進行間接溝通。
- 觀察者允許接收者動态地訂閱或取消接收請求。
-
責任鍊的管理者可使用指令模式實作。 在這種情況下, 你可以對由請求代表的同一個上下文對象執行許多不同的操作。
還有另外一種實作方式, 那就是請求自身就是一個指令對象。 在這種情況下, 你可以對由一系列不同上下文連接配接而成的鍊執行相同的操作。
- 你可以同時使用指令和備忘錄模式來實作 “撤銷”。 在這種情況下, 指令用于對目标對象執行各種不同的操作, 備忘錄用來儲存一條指令執行前該對象的狀态。
- 指令和政策模式看上去很像, 因為兩者都能通過某些行為來參數化對象。 但是, 它們的意圖有非常大的不同。
- 你可以使用指令來将任何操作轉換為對象。 操作的參數将成為對象的成員變量。 你可以通過轉換來延遲操作的執行、 将操作放入隊列、 儲存曆史指令或者向遠端服務發送指令等。
- 另一方面, 政策通常可用于描述完成某件事的不同方式, 讓你能夠在同一個上下文類中切換算法。
- 原型模式可用于儲存指令的曆史記錄。
- 你可以将通路者模式視為指令模式的加強版本, 其對象可對不同類的多種對象執行操作。
代碼示例
在本例中, 指令模式會記錄已執行操作的曆史記錄, 以在需要時撤銷操作。
文本編輯器中的可撤銷操作。
有些指令會改變編輯器的狀态 (例如剪切和粘貼), 它們可在執行相關操作前對編輯器的狀态進行備份。 指令執行後會和目前點備份的編輯器狀态一起被放入指令曆史 (指令對象棧)。 此後, 如果使用者需要進行復原操作, 程式可從曆史記錄中取出最近的指令, 讀取相應的編輯器狀态備份, 然後進行恢複。
用戶端代碼 (GUI 元素和指令曆史等) 沒有和具體指令類相耦合, 因為它通過指令接口來使用指令。 這使得你能在無需修改已有代碼的情況下在程式中增加新的指令。
package com.qiang.pei.ding.behavioralPatterns.command.demo1.button;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.CommandHistory;
public abstract class AbstractButton {
Command command;
public abstract void operation();
public void setCommand(Command command){
this.command = command;
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.button;
public class CopyButton extends AbstractButton {
@Override
public void operation() {
this.command.executeCommand();
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.button;
public class CutButton extends AbstractButton {
@Override
public void operation() {
this.command.executeCommand();
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.button;
public class PasteButton extends AbstractButton {
@Override
public void operation() {
this.command.executeCommand();
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.button;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
import java.util.HashMap;
import java.util.Map;
public class ShortCuts{
Map<String,Command> shortCuts = new HashMap<String,Command>();
public void onKeyPress(String shortCut, Command command){
shortCuts.put(shortCut,command);
}
public void operation(String shortCut) {
Command command = shortCuts.get(shortCut);
if(command!=null){
command.executeCommand();
}else{
System.out.println("shortCut="+shortCut+"快捷方式不存在");
}
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.button;
public class UndoButton extends AbstractButton {
@Override
public void operation() {
this.command.executeCommand();
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Application;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Editor;
指令基類會為所有具體指令定義通用接口。
public abstract class Command {
protected Application app;
protected Editor editor;
protected String backup;
CommandHistory history;
public Command(Application app , Editor editor,CommandHistory history ) {
this.app = app;
this.editor = editor ;
this.history = history ;
}
// 備份編輯器狀态。
public void saveBackup() {
backup = editor.text;
}
// 恢複編輯器狀态。
public void undo() {
System.out.println("undo--------");
editor.text = backup ;
}
// 從曆史記錄中取出最近的指令并運作其 undo(撤銷)方法。請注意,你并
// 不知曉該指令所屬的類。但是我們不需要知曉,因為指令自己知道如何撤銷
// 其動作。
public void undoback() {
Command command = history.pop();
if (command != null)
command.undo() ;
}
// 執行方法被聲明為抽象以強制所有具體指令提供自己的實作。該方法必須根
// 據指令是否更改編輯器的狀态傳回 true 或 false。
public abstract boolean execute();
// 執行一個指令并檢查它是否需要被添加到曆史記錄中。
public void executeCommand() {
if (this.execute()) {
history.push(this);
}
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
import java.util.Stack;
// 全局指令曆史記錄就是一個堆桟。
public class CommandHistory {
private Stack<Command> history = new Stack<Command>() ;
// 将指令壓入曆史記錄數組的末尾。
public void push(Command c) {
history.push(c);
}
// ...先出 // 從曆史記錄中取出最近的指令。
public Command pop(){
return history.pop();
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Application;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Editor;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
public class CopyCommand extends Command {
public CopyCommand(Application app, Editor editor,CommandHistory history) {
super(app, editor,history);
}
// 複制指令不會被儲存到曆史記錄中,因為它沒有改變編輯器的狀态。
@Override
public boolean execute() {
app.clipboard = editor.getSelection();
return false;
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Application;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Editor;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
public class CutCommand extends Command {
public CutCommand(Application app, Editor editor,CommandHistory history) {
super(app, editor,history);
}
// 剪切指令改變了編輯器的狀态,是以它必須被儲存到曆史記錄中。隻要方法
// 傳回 true,它就會被儲存。
@Override
public boolean execute() {
saveBackup();
app.clipboard = editor.getSelection();
editor.deleteSelection();
return true;
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Application;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Editor;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
public class PasteCommand extends Command {
public PasteCommand(Application app, Editor editor,CommandHistory history) {
super(app, editor,history);
}
@Override
public boolean execute() {
saveBackup();
editor.replaceSelection(app.clipboard);
System.out.println("PasteCommand--------------------");
return true ;
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1.command;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Application;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.Editor;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.Command;
public class UndoCommand extends Command {
public UndoCommand(Application app, Editor editor,CommandHistory history) {
super(app, editor,history);
}
@Override
public boolean execute() {
System.out.println("UndoCommand--------");
undoback();
return false;
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1;
// 編輯器類包含實際的文本編輯操作。它會擔任接收者的角色:最後所有指令都會
// 将執行工作委派給編輯器的方法。
public class Editor {
public String text;
// 傳回選中的文字。
public String getSelection(){
return text;
}
// 删除選中的文字。
public void deleteSelection(){
text="";
}
在目前位置插入剪貼闆中的内容
public void replaceSelection(String text){
}
}
package com.qiang.pei.ding.behavioralPatterns.command.demo1;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.button.*;
import com.qiang.pei.ding.behavioralPatterns.command.demo1.command.*;
import java.util.ArrayList;
// 應用程式類會設定對象之間的關系。它會擔任發送者的角色:當需要完成某些工
// 作時,它會建立并執行一個指令對象。
public class Application {
public String clipboard;
ArrayList<Editor> editors;
Editor activeEditor;
CopyButton copyButton;
ShortCuts shortcuts;
CutButton cutButton;
PasteButton pasteButton;
UndoButton undoButton;
CommandHistory history;
public Application(){
this.activeEditor= new Editor();
activeEditor.text="1111111111111";
this.history= new CommandHistory();
this.copyButton= new CopyButton();
this.shortcuts= new ShortCuts();
this.cutButton= new CutButton();
this.pasteButton= new PasteButton();
this.undoButton= new UndoButton();
}
public void createUI() {
// ...
Command copy = new CopyCommand(this, activeEditor,history) ;
copyButton.setCommand(copy);
shortcuts.onKeyPress("Ctrl+C", copy);
Command cut =
new CutCommand(this, activeEditor,history);
cutButton.setCommand(cut);
shortcuts.onKeyPress("Ctrl+X", cut);
Command paste =
new PasteCommand(this, activeEditor,history);
pasteButton.setCommand(paste);
shortcuts.onKeyPress("Ctrl+V", paste);
Command undo =
new UndoCommand(this, activeEditor,history);
undoButton.setCommand(undo);
shortcuts.onKeyPress("Ctrl+Z", undo);
}
public static void main(String[] args) {
Application app = new Application();
app.createUI();
app.pasteButton.operation();
app.shortcuts.operation("Ctrl+V");
}
}