意圖
指令是一種行為設計模式,它将請求轉化為一個獨立的對象,該對象包含了關于請求的所有資訊。這種轉化使得你可以将請求作為方法參數傳遞,延遲或排隊請求的執行,并支援可撤銷的操作。
問題
假設你正在開發一個新的文本編輯器應用。你目前的任務是建立一個工具欄,其中包含了一系列用于編輯器各種操作的按鈕。你已經建立了一個非常整潔的Button類,可以用于工具欄上的按鈕,也可以用于各種對話框中的通用按鈕。
所有應用程式的按鈕都派生自同一個類。
雖然所有這些按鈕看起來相似,但它們都應該執行不同的操作。你會把各種按鈕的點選處理代碼放在哪裡?最簡單的解決方案是為每個按鈕使用的地方建立大量的子類。這些子類将包含在按鈕點選時需要執行的代碼。
衆多按鈕子類,可能出什麼差錯呢?
不久之後,您會意識到這種方法存在深刻的缺陷。首先,您擁有大量的子類,如果您每次修改基礎按鈕類時都面臨着破壞這些子類中的代碼的風險,那将是不可取的。簡而言之,您的GUI代碼過于依賴易變的業務邏輯代碼,這使得整個代碼變得笨拙。
有幾個類實作了相同的功能。
而這裡最不美觀的部分就是:某些操作,比如複制/粘貼文本,需要從多個位置調用。例如,使用者可以在工具欄上點選一個小的“複制”按鈕,或通過上下文菜單複制某些内容,或者隻需在鍵盤上按下Ctrl+C鍵。
最初,當我們的應用程式隻有工具欄時,将各種操作的實作放入按鈕子類中是可以的。換句話說,在CopyButton子類中放置複制文本的代碼是可以接受的。但是,當您實作上下文菜單、快捷鍵和其他内容時,您要麼必須在許多類中複制操作的代碼,要麼使菜單依賴于按鈕,而這甚至是更糟糕的選擇。
解決方法
良好的軟體設計通常基于關注點分離的原則,這通常會将應用程式分成不同的層次。最常見的例子是将應用程式分為圖形使用者界面層和業務邏輯層。GUI層負責在螢幕上呈現美觀的圖像,捕捉任何輸入,并顯示使用者和應用程式的操作結果。然而,當涉及到執行重要任務,比如計算月球軌迹或撰寫年度報告時,GUI層會将工作委托給底層的業務邏輯層。
在代碼中,這可能會像這樣:GUI對象調用業務邏輯對象的方法,并傳遞一些參數。這個過程通常被描述為一個對象向另一個對象發送請求。
GUI對象可以直接通路業務邏輯對象。
指令模式建議GUI對象不應直接發送這些請求。相反,您應該将所有請求的細節,例如被調用的對象、方法的名稱和參數清單,提取到一個單獨的指令類中,該類具有一個方法來觸發該請求。
指令對象作為GUI和業務邏輯對象之間的連結。從現在開始,GUI對象無需知道哪個業務邏輯對象将接收請求以及如何處理請求。GUI對象隻需觸發指令,而指令會處理所有細節。
通過指令來通路業務邏輯層。
下一步是使您的指令實作相同的接口。通常,它隻有一個執行方法,不接受任何參數。這個接口使您能夠在相同的請求發送者中使用各種指令,而不将其與具體的指令類耦合。作為額外的好處,現在您可以在運作時切換與發送者關聯的指令對象,有效地改變發送者的行為。
您可能注意到了一個缺失的部分,即請求參數。GUI對象可能會向業務層對象提供一些參數。由于指令執行方法沒有任何參數,我們如何将請求細節傳遞給接收者呢?事實證明,指令應該預先配置這些資料,或者能夠自行擷取這些資料。
GUI對象将工作委托給指令對象。
讓我們回到我們的文本編輯器。在應用指令模式之後,我們不再需要所有那些按鈕子類來實作各種點選行為。隻需在基礎Button類中放置一個字段,用于存儲對指令對象的引用,并在按鈕點選時執行該指令即可。
您将為每個可能的操作實作一組指令類,并将它們與特定按鈕關聯起來,具體取決于按鈕的預期行為。
其他GUI元素,如菜單、快捷鍵或整個對話框,也可以以同樣的方式實作。它們将與一個指令連結在一起,在使用者與GUI元素互動時執行該指令。正如您現在可能已經猜到的那樣,與相同操作相關的元素将連結到相同的指令,防止任何代碼重複。
結果,指令成為一個友善的中間層,減少了GUI和業務邏輯層之間的耦合。這隻是指令模式可以提供的一小部分好處!
真實世界類比
在餐廳點餐
漫長的城市漫步之後,您來到了一家不錯的餐廳,坐在靠窗的桌子旁。一位友好的服務員走過來,迅速地接受您的點單,并将其記在一張紙上。服務員将點單貼在牆上,然後去了廚房。過了一會兒,廚師拿到了點單,閱讀并根據點單烹制餐點。廚師将餐點與點單一起放在托盤上。服務員發現托盤,檢查點單以確定您的要求得到滿足,然後将一切端到您的桌子上。
這張紙負責傳達指令。它會保留在隊列中,直到廚師準備好為您服務。點單包含了所有烹制餐點所需的相關資訊。這樣一來,廚師可以立即開始烹饪,而無需直接與您确認訂單細節。
結構
1、發送者類(也稱為調用者)負責發起請求。該類必須有一個字段用于存儲對指令對象的引用。發送者觸發該指令,而不是直接将請求發送給接收者。請注意,發送者不負責建立指令對象。通常情況下,它通過構造函數從用戶端擷取一個預先建立的指令對象。
2、指令接口通常僅聲明一個執行指令的方法。
3、具體指令實作各種類型的請求。具體指令不應該自行執行工作,而是将調用傳遞給業務邏輯對象之一。然而,為了簡化代碼,這些類可以合并。
在具體指令中,可以将執行接收對象上的方法所需的參數聲明為字段。通過僅允許通過構造函數初始化這些字段,可以使指令對象不可變。
4、接收者類包含一些業務邏輯。幾乎任何對象都可以充當接收者。大多數指令隻處理将請求傳遞給接收者的細節,而接收者本身執行實際的工作。
5、用戶端建立和配置具體指令對象。用戶端必須将所有請求參數(包括接收者執行個體)傳遞給指令的構造函數。之後,生成的指令可以與一個或多個發送者關聯起來。
僞代碼
在這個例子中,指令模式有助于跟蹤執行操作的曆史,并在需要時可以撤銷操作。
文本編輯器中的可撤銷操作。
在改變編輯器狀态的指令(例如剪切和粘貼)執行相關操作之前,會對編輯器的狀态進行備份。指令執行後,将其與執行該指令時的編輯器狀态備份一起放入指令曆史記錄(指令對象的堆棧)中。如果使用者需要撤銷操作,應用程式可以從曆史記錄中擷取最近的指令,讀取相關的編輯器狀态備份并進行恢複。
用戶端代碼(GUI元素、指令曆史記錄等)不與具體的指令類耦合,因為它通過指令接口來處理指令。這種方法使您能夠在應用程式中引入新的指令,而不會破壞任何現有代碼。
// The base command class defines the common interface for all
// concrete commands.
abstract class Command is
protected field app: Application
protected field editor: Editor
protected field backup: text
constructor Command(app: Application, editor: Editor) is
this.app = app
this.editor = editor
// Make a backup of the editor's state.
method saveBackup() is
backup = editor.text
// Restore the editor's state.
method undo() is
editor.text = backup
// The execution method is declared abstract to force all
// concrete commands to provide their own implementations.
// The method must return true or false depending on whether
// the command changes the editor's state.
abstract method execute()
// The concrete commands go here.
class CopyCommand extends Command is
// The copy command isn't saved to the history since it
// doesn't change the editor's state.
method execute() is
app.clipboard = editor.getSelection()
return false
class CutCommand extends Command is
// The cut command does change the editor's state, therefore
// it must be saved to the history. And it'll be saved as
// long as the method returns true.
method execute() is
saveBackup()
app.clipboard = editor.getSelection()
editor.deleteSelection()
return true
class PasteCommand extends Command is
method execute() is
saveBackup()
editor.replaceSelection(app.clipboard)
return true
// The undo operation is also a command.
class UndoCommand extends Command is
method execute() is
app.undo()
return false
// The global command history is just a stack.
class CommandHistory is
private field history: array of Command
// Last in...
method push(c: Command) is
// Push the command to the end of the history array.
// ...first out
method pop():Command is
// Get the most recent command from the history.
// The editor class has actual text editing operations. It plays
// the role of a receiver: all commands end up delegating
// execution to the editor's methods.
class Editor is
field text: string
method getSelection() is
// Return selected text.
method deleteSelection() is
// Delete selected text.
method replaceSelection(text) is
// Insert the clipboard's contents at the current
// position.
// The application class sets up object relations. It acts as a
// sender: when something needs to be done, it creates a command
// object and executes it.
class Application is
field clipboard: string
field editors: array of Editors
field activeEditor: Editor
field history: CommandHistory
// The code which assigns commands to UI objects may look
// like this.
method createUI() is
// ...
copy = function() { executeCommand(
new CopyCommand(this, activeEditor)) }
copyButton.setCommand(copy)
shortcuts.onKeyPress("Ctrl+C", copy)
cut = function() { executeCommand(
new CutCommand(this, activeEditor)) }
cutButton.setCommand(cut)
shortcuts.onKeyPress("Ctrl+X", cut)
paste = function() { executeCommand(
new PasteCommand(this, activeEditor)) }
pasteButton.setCommand(paste)
shortcuts.onKeyPress("Ctrl+V", paste)
undo = function() { executeCommand(
new UndoCommand(this, activeEditor)) }
undoButton.setCommand(undo)
shortcuts.onKeyPress("Ctrl+Z", undo)
// Execute a command and check whether it has to be added to
// the history.
method executeCommand(command) is
if (command.execute)
history.push(command)
// Take the most recent command from the history and run its
// undo method. Note that we don't know the class of that
// command. But we don't have to, since the command knows
// how to undo its own action.
method undo() is
command = history.pop()
if (command != null)
command.undo()
适用範圍
當您想要使用操作對對象進行參數化時,可以使用指令模式。
指令模式可以将特定的方法調用轉化為獨立的對象。這種改變帶來了許多有趣的用途:您可以将指令作為方法參數傳遞,将其存儲在其他對象中,動态切換關聯的指令等。
例如,您正在開發一個圖形使用者界面元件,比如一個上下文菜單,您希望使用者能夠配置菜單項,在最終使用者點選菜單項時觸發相應的操作。
當您想要排隊操作、排程它們的執行或者在遠端執行它們時,可以使用指令模式。
與任何其他對象一樣,指令可以被序列化,也就是将其轉換為一個可以輕松寫入檔案或資料庫的字元串。稍後,可以将該字元串還原為初始的指令對象。是以,您可以延遲和排程指令的執行。而且更棒的是!以同樣的方式,您可以對指令進行排隊、記錄或通過網絡發送。
當您想要實作可逆操作時,可以使用指令模式。
雖然有許多實作撤銷/重做的方式,但指令模式可能是最受歡迎的一種。
為了能夠撤銷操作,您需要實作已執行操作的曆史記錄。指令曆史記錄是一個棧,其中包含所有已執行的指令對象以及應用程式狀态的相關備份。
該方法有兩個缺點。首先,儲存應用程式狀态并不那麼容易,因為其中一些狀态可能是私有的。這個問題可以通過備忘錄模式來緩解。
其次,狀态備份可能會占用相當多的記憶體。是以,有時可以采用替代性實作:指令執行相反的操作,而不是恢複先前的狀态。然而,相反操作也有一個代價:它可能很難,甚至不可能實作。
如何實作
1、聲明具有單個執行方法的指令接口。
2、将請求提取到實作指令接口的具體指令類中。每個類必須具有一組字段,用于存儲請求的參數以及對實際接收器對象的引用。所有這些值必須通過指令的構造函數進行初始化。
3、識别将充當發送者的類。将存儲指令的字段添加到這些類中。發送者應僅通過指令接口與其指令進行通信。發送者通常不會自己建立指令對象,而是從用戶端代碼中擷取它們。
4、更改發送者,使其執行指令,而不是直接向接收器發送請求。
5、用戶端應按以下順序初始化對象:
- 建立接收器。
- 建立指令,并根據需要将其與接收器關聯。
- 建立發送者,并将其與特定的指令關聯。
Python示例
from __future__ import annotations
from abc import ABC, abstractmethod
class Command(ABC):
"""
The Command interface declares a method for executing a command.
"""
@abstractmethod
def execute(self) -> None:
pass
class SimpleCommand(Command):
"""
Some commands can implement simple operations on their own.
"""
def __init__(self, payload: str) -> None:
self._payload = payload
def execute(self) -> None:
print(f"SimpleCommand: See, I can do simple things like printing"
f"({self._payload})")
class ComplexCommand(Command):
"""
However, some commands can delegate more complex operations to other
objects, called "receivers."
"""
def __init__(self, receiver: Receiver, a: str, b: str) -> None:
"""
Complex commands can accept one or several receiver objects along with
any context data via the constructor.
"""
self._receiver = receiver
self._a = a
self._b = b
def execute(self) -> None:
"""
Commands can delegate to any methods of a receiver.
"""
print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
self._receiver.do_something(self._a)
self._receiver.do_something_else(self._b)
class Receiver:
"""
The Receiver classes contain some important business logic. They know how to
perform all kinds of operations, associated with carrying out a request. In
fact, any class may serve as a Receiver.
"""
def do_something(self, a: str) -> None:
print(f"\nReceiver: Working on ({a}.)", end="")
def do_something_else(self, b: str) -> None:
print(f"\nReceiver: Also working on ({b}.)", end="")
class Invoker:
"""
The Invoker is associated with one or several commands. It sends a request
to the command.
"""
_on_start = None
_on_finish = None
"""
Initialize commands.
"""
def set_on_start(self, command: Command):
self._on_start = command
def set_on_finish(self, command: Command):
self._on_finish = command
def do_something_important(self) -> None:
"""
The Invoker does not depend on concrete command or receiver classes. The
Invoker passes a request to a receiver indirectly, by executing a
command.
"""
print("Invoker: Does anybody want something done before I begin?")
if isinstance(self._on_start, Command):
self._on_start.execute()
print("Invoker: ...doing something really important...")
print("Invoker: Does anybody want something done after I finish?")
if isinstance(self._on_finish, Command):
self._on_finish.execute()
if __name__ == "__main__":
"""
The client code can parameterize an invoker with any commands.
"""
invoker = Invoker()
invoker.set_on_start(SimpleCommand("Say Hi!"))
receiver = Receiver()
invoker.set_on_finish(ComplexCommand(
receiver, "Send email", "Save report"))
invoker.do_something_important()