Command 命令
意圖
Command 是一種行爲設計模式,它將請求轉換爲包含有關請求的所有信息的獨立對象。這種轉換允許您將請求作爲方法參數傳遞,延遲或排隊請求的執行,並支持可撤消的操作。
問題
假設您正在開發一個新的文本編輯器應用程序,您當前的任務是創建一個工具欄,其中包含一系列用於編輯器各種操作的按鈕。您創建了一個非常簡潔的 假設您正在開發一個新的文本編輯器應用程序,您當前的任務是創建一個工具欄,其中包含一系列用於編輯器各種操作的按鈕。您創建了一個非常簡潔的 Button
類,可用於工具欄上的按鈕,以及各種對話框中的通用按鈕。 類,可用於工具欄上的按鈕,以及各種對話框中的通用按鈕。
應用程序的所有按鈕都派生自同一個類。
雖然所有這些按鈕看起來都很相似,但它們都應該做不同的事情。您將把這些按鈕的各種單擊處理程序的代碼放在哪裏?最簡單的解決方案是爲每個使用按鈕的地方創建大量的子類。這些子類將包含必須在單擊按鈕時執行的代碼。
應用程序的所有按鈕都派生自同一個類。
不久,你就會意識到這種方法有很大的缺陷。首先,您有大量的子類,如果您每次修改基類 不久,你就會意識到這種方法有很大的缺陷。首先,您有大量的子類,如果您每次修改基類 Button
時不會有破壞這些子類中代碼的風險,那麼這是可以的。簡單地說,GUI 代碼已經變得笨拙地依賴於業務邏輯的易變代碼。 時不會有破壞這些子類中代碼的風險,那麼這是可以的。簡單地說,GUI 代碼已經變得笨拙地依賴於業務邏輯的易變代碼。
多個類實現相同的功能。
最醜陋的是。某些操作,如複製 / 粘貼文本,需要從多個位置調用。例如,用戶可以點擊工具欄上的一個小的 “複製” 按鈕,或者通過上下文菜單複製一些東西,或者只是點擊鍵盤上的 最醜陋的是。某些操作,如複製 / 粘貼文本,需要從多個位置調用。例如,用戶可以點擊工具欄上的一個小的 “複製” 按鈕,或者通過上下文菜單複製一些東西,或者只是點擊鍵盤上的 Ctrl+C
。
最初,當我們的應用程序只有工具欄時,可以將各種操作的實現放在按鈕子類中。換句話說,在 最初,當我們的應用程序只有工具欄時,可以將各種操作的實現放在按鈕子類中。換句話說,在 CopyButton
子類中複製文本的代碼是可以的。但是,當您實現上下文菜單、快捷方式和其他東西時,您必須在許多類中複製操作代碼,或者使菜單依賴於按鈕,這是一個更糟糕的選擇。 子類中複製文本的代碼是可以的。但是,當您實現上下文菜單、快捷方式和其他東西時,您必須在許多類中複製操作代碼,或者使菜單依賴於按鈕,這是一個更糟糕的選擇。
解決方案
好的軟件設計通常基於關注點分離的原則,這通常會導致將應用程序分解爲多個層。最常見的例子:一層用於圖形用戶界面,另一層用於業務邏輯。GUI 層負責在屏幕上呈現美麗的圖片,捕獲任何輸入並顯示用戶和應用程序正在執行的操作的結果。然而,當涉及到做一些重要的事情時,比如計算月球的軌跡或撰寫年度報告,GUI 層將工作委託給業務邏輯的底層。
在代碼中,它可能看起來像這樣:GUI 對象調用業務邏輯對象的方法,並向其傳遞一些參數。這個過程通常被描述爲一個對象向另一個對象發送請求。
GUI 對象可以直接訪問業務邏輯對象。
命令模式建議 GUI 對象不應該直接發送這些請求。相反,您應該提取所有請求的詳細信息,例如被調用的對象,方法的名稱和參數列表到一個單獨的命令類中,並使用一個觸發此請求的方法。
命令對象充當各種 GUI 和業務邏輯對象之間的鏈接。從現在開始,GUI 對象不需要知道什麼業務邏輯對象將接收請求以及如何處理請求。GUI 對象只是觸發命令,該命令處理所有細節。
通過命令調用業務邏輯層。
下一步是使您的命令實現相同的接口。通常它只有一個不帶參數的執行方法。這個接口允許您對同一個請求發送者使用不同的命令,而無需將其耦合到具體的命令類。作爲獎勵,現在您可以切換鏈接到發送者的命令對象,有效地改變發送者在運行時的行爲。
您可能已經注意到了這個難題中缺少的一個部分,即請求參數。GUI 對象可能已經爲業務層對象提供了一些參數。由於命令執行方法沒有任何參數,我們如何將請求細節傳遞給接收方?事實證明,該命令應該預先配置了這些數據,或者能夠自己獲取這些數據。
通過命令調用業務邏輯層。
讓我們回到我們的文本編輯器。在我們應用 Command 模式之後,我們不再需要所有那些按鈕子類來實現各種單擊行爲。將一個字段放入存儲命令對象引用的基類 讓我們回到我們的文本編輯器。在我們應用 Command 模式之後,我們不再需要所有那些按鈕子類來實現各種單擊行爲。將一個字段放入存儲命令對象引用的基類 Button
中,並使按鈕在單擊時執行該命令就足夠了。 中,並使按鈕在單擊時執行該命令就足夠了。
您將爲每個可能的操作實現一組命令類,並根據按鈕的預期行爲將它們與特定按鈕鏈接。
其他 GUI 元素,如菜單、快捷方式或整個對話框,也可以用同樣的方式實現。它們將被鏈接到一個命令,當用戶與 GUI 元素交互時,該命令將被執行。正如您現在可能已經猜到的,與相同操作相關的元素將鏈接到相同的命令,從而防止任何代碼重複。
因此,命令成爲一個方便的中間層,減少了 GUI 和業務邏輯層之間的耦合。而這只是命令模式所能提供的好處的一小部分!
現實世界的類比
在餐館點菜。
在城市裏走了很長一段路後,你來到一家不錯的餐館,坐在靠窗的桌子旁。一個友好的服務員走近你,迅速地把你的訂單寫在一張紙上。服務員走到廚房,把菜單貼在牆上。過了一段時間,訂單到達廚師,誰讀它和烹飪相應的飯菜。廚師把飯菜和點菜單一起放在托盤上沿着。服務員發現托盤,檢查訂單,以確保一切都是你想要的,並把一切都帶到你的桌子上。
紙上的命令是命令。在廚師準備上菜之前,它一直處於排隊狀態。訂單包含了烹飪這頓飯所需的所有相關信息。它允許廚師立即開始烹飪,而不是跑來跑去直接向您澄清訂單細節。
結構
-
1. 類(也稱爲 invoker)負責發起請求。此類必須有一個用於存儲對命令對象的引用的字段。發送方觸發該命令,而不是直接向接收方發送請求。請注意,發送方不負責創建命令對象。通常,它通過構造函數從客戶端獲取預先創建的命令。
-
2. Command 接口通常只聲明一個執行命令的方法。
-
3. 在接收對象上執行方法所需的參數可以在具體命令中聲明爲字段。通過只允許通過構造函數初始化這些字段,可以使命令對象不可變。
-
4. Receiver 類包含一些業務邏輯。幾乎任何物體都可以充當接收器。大多數命令只處理如何將請求傳遞給接收方的細節,而接收方本身則執行實際工作。
-
5. 客戶端創建和配置具體的命令對象。客戶端必須將所有請求參數(包括接收器實例)傳遞到命令的構造函數中。在此之後,所得到的命令可以與一個或多個命令相關聯。
僞代碼
在本例中,Command 模式有助於跟蹤已執行操作的歷史記錄,並在需要時恢復操作。
文本編輯器中可撤消的操作。
導致更改編輯器狀態的命令(例如,剪切和粘貼)在執行與該命令相關聯的操作之前製作編輯器狀態的備份副本。在命令執行之後,它將與編輯器當時狀態的備份副本一起沿着放置到命令歷史記錄(命令對象的堆棧)中。稍後,如果用戶需要恢復操作,應用可以從歷史記錄中獲取最新的命令,讀取編輯器狀態的相關備份,然後將其恢復。
客戶端代碼(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()
適用性
-
如果要通過操作參數化對象,請使用 Command 模式。
-
Command 模式可以將特定的方法調用轉換爲獨立的對象。這一變化開啓了許多有趣的用途:你可以將命令作爲方法參數傳遞,將它們存儲在其他對象中,在運行時切換鏈接的命令,等等。下面是一個示例:您正在開發一個 GUI 組件(如上下文菜單),並且希望您的用戶能夠配置菜單項,以便在最終用戶單擊某項時觸發操作。
-
當您希望將操作排隊、計劃其執行或遠程執行時,請使用 Command 模式。
-
與任何其他對象一樣,命令可以序列化,這意味着將其轉換爲可以輕鬆寫入文件或數據庫的字符串。稍後,該字符串可以恢復爲初始命令對象。因此,您可以延遲和調度命令的執行。但還有更多!以同樣的方式,您可以通過網絡排隊,記錄或發送命令。
-
當你想實現可逆操作時,使用命令模式。
-
• 雖然有很多方法可以實現撤銷 / 重做,但命令模式可能是最流行的。爲了能夠還原操作,您需要實現已執行操作的歷史記錄。命令歷史記錄是一個堆棧,其中包含所有已執行的命令對象沿着以及應用程序狀態的相關備份。這種方法有兩個缺點。首先,保存應用程序的狀態並不容易,因爲其中一些狀態可能是私有的。這個問題可以通過 Memento 模式來緩解。其次,狀態備份可能會消耗大量 RAM。因此,有時您可以採用另一種實現方式:命令執行相反的操作,而不是恢復過去的狀態。反向操作也有代價:它可能很難甚至不可能實施。
如何實現
-
1. 使用單個執行方法 decompose 命令接口。
-
2. 開始將請求提取到實現命令接口的具體命令類中。每個類都必須有一組字段,用於存儲請求參數沿着對實際接收器對象的引用。所有這些值都必須通過命令的構造函數初始化。
-
3. 確定將充當代理的類。將用於存儲命令的字段添加到這些類中。發送者應僅通過命令接口與其命令進行通信。發送方通常不會自己創建命令對象,而是從客戶端代碼中獲取命令對象。
-
4. 更改發送方,使其執行命令,而不是直接向接收方發送請求。
-
5. 客戶端應按以下順序初始化對象:
-
• 創建接收器。
-
• 創建命令,並在需要時將它們與接收器相關聯。
-
• 創建命令行,並將它們與特定命令關聯。
利弊
與其他模式的關係
-
責任鏈、命令、調解器和觀察者解決了連接請求的接收者和接收者的各種方式:
-
責任鏈(Chain of Responsibility)將請求順序地沿着一個動態的潛在接收者鏈傳遞,直到其中一個接收者處理它。
-
命令在中繼器和接收器之間建立單向連接。
-
Mediator 消除了發送者和接收者之間的直接連接,迫使它們通過 Mediator 對象間接通信。
-
觀察者允許接收者動態訂閱和取消訂閱接收請求。
-
責任鏈中的處理程序可以作爲命令實現。在這種情況下,您可以對同一個上下文對象(由請求表示)執行許多不同的操作。然而,還有另一種方法,其中請求本身是一個 Command 對象。在這種情況下,您可以在鏈接成鏈的一系列不同上下文中執行相同的操作。
-
你可以使用命令和 Memento 一起實現 “撤消”。在這種情況下,命令負責對目標對象執行各種操作,而 memento 則在執行命令之前保存該對象的狀態。
-
Command 和 Strategy 可能看起來很相似,因爲您可以使用這兩種方法通過某些操作來參數化對象。然而,他們有非常不同的意圖。
-
您可以使用 Command 將任何操作轉換爲對象。操作的參數成爲該對象的字段。轉換允許您延遲操作的執行,將其排隊,存儲命令的歷史記錄,將命令發送到遠程服務等。
-
另一方面,Strategy 通常描述做同一件事的不同方法,讓你在一個上下文類中交換這些算法。
-
當您需要將命令的副本保存到歷史記錄中時,Prototype 可以提供幫助。
-
您可以將 Visitor 視爲 Command 模式的強大版本。它的對象可以在不同類的各種對象上執行操作。
Python 中的 Command
命令是一種行爲設計模式,它將請求或簡單操作轉換爲對象。
轉換允許延遲或遠程執行命令,存儲命令歷史等。
概念示例
這個例子說明了命令設計模式的結構。它側重於回答這些問題:
-
• 它由哪些類組成?
-
• 這些班級扮演什麼角色?
-
• 模式中的元素是以什麼方式聯繫在一起的?
main.py:概念性示例
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()
Output.txt:執行結果
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)
Command in Rust
命令是一種行爲設計模式,它將請求或簡單操作轉換爲對象。
轉換允許延遲或遠程執行命令,存儲命令歷史等。
在 Rust 中,命令實例不應該持有對全局上下文的永久引用,相反,後者應該作爲 “ 在 Rust 中,命令實例不應該持有對全局上下文的永久引用,相反,後者應該作爲 “ execute
“方法的可變參數從上到下傳遞: “方法的可變參數從上到下傳遞:
fn execute(&mut self, app: &mut cursive::Cursive) -> bool;
文本編輯器:命令和撤消
關鍵點:
-
每個按鈕運行一個單獨的命令。
-
由於命令被表示爲對象,因此可以將其推入 由於命令被表示爲對象,因此可以將其推入
history
數組,以便稍後撤消。 數組,以便稍後撤消。 -
TUI 使用 TUI 使用
cursive
crate 創建。 crate 創建。
command.rs:Command Interface
mod copy;
mod cut;
mod paste;
pub use copy::CopyCommand;
pub use cut::CutCommand;
pub use paste::PasteCommand;
/// Declares a method for executing (and undoing) a command.
///
/// Each command receives an application context to access
/// visual components (e.g. edit view) and a clipboard.
pub trait Command {
fn execute(&mut self, app: &mut cursive::Cursive) -> bool;
fn undo(&mut self, app: &mut cursive::Cursive);
}
command/copy.rs:複製命令
use cursive::{views::EditView, Cursive};
use super::Command;
use crate::AppContext;
#[derive(Default)]
pub struct CopyCommand;
impl Command for CopyCommand {
fn execute(&mut self, app: &mut Cursive) -> bool {
let editor = app.find_name::<EditView>("Editor").unwrap();
let mut context = app.take_user_data::<AppContext>().unwrap();
context.clipboard = editor.get_content().to_string();
app.set_user_data(context);
false
}
fn undo(&mut self, _: &mut Cursive) {}
}
command/cut.rs:剪切命令
use cursive::{views::EditView, Cursive};
use super::Command;
use crate::AppContext;
#[derive(Default)]
pub struct CutCommand {
backup: String,
}
impl Command for CutCommand {
fn execute(&mut self, app: &mut Cursive) -> bool {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
app.with_user_data(|context: &mut AppContext| {
self.backup = editor.get_content().to_string();
context.clipboard = self.backup.clone();
editor.set_content("".to_string());
});
true
}
fn undo(&mut self, app: &mut Cursive) {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
editor.set_content(&self.backup);
}
}
command/paste.rs:粘貼命令
use cursive::{views::EditView, Cursive};
use super::Command;
use crate::AppContext;
#[derive(Default)]
pub struct PasteCommand {
backup: String,
}
impl Command for PasteCommand {
fn execute(&mut self, app: &mut Cursive) -> bool {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
app.with_user_data(|context: &mut AppContext| {
self.backup = editor.get_content().to_string();
editor.set_content(context.clipboard.clone());
});
true
}
fn undo(&mut self, app: &mut Cursive) {
let mut editor = app.find_name::<EditView>("Editor").unwrap();
editor.set_content(&self.backup);
}
}
main.rs:客戶端代碼
mod command;
use cursive::{
traits::Nameable,
views::{Dialog, EditView},
Cursive,
};
use command::{Command, CopyCommand, CutCommand, PasteCommand};
/// An application context to be passed into visual component callbacks.
/// It contains a clipboard and a history of commands to be undone.
#[derive(Default)]
struct AppContext {
clipboard: String,
history: Vec<Box<dyn Command>>,
}
fn main() {
let mut app = cursive::default();
app.set_user_data(AppContext::default());
app.add_layer(
Dialog::around(EditView::default().with_name("Editor"))
.title("Type and use buttons")
.button("Copy", |s| execute(s, CopyCommand::default()))
.button("Cut", |s| execute(s, CutCommand::default()))
.button("Paste", |s| execute(s, PasteCommand::default()))
.button("Undo", undo)
.button("Quit", |s| s.quit()),
);
app.run();
}
/// Executes a command and then pushes it to a history array.
fn execute(app: &mut Cursive, mut command: impl Command + 'static) {
if command.execute(app) {
app.with_user_data(|context: &mut AppContext| {
context.history.push(Box::new(command));
});
}
}
/// Pops the last command and executes an undo action.
fn undo(app: &mut Cursive) {
let mut context = app.take_user_data::<AppContext>().unwrap();
if let Some(mut command) = context.history.pop() {
command.undo(app)
}
app.set_user_data(context);
}
Output 輸出
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vcqfEKs-o2DY7IOqN9Jy8A