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. 1. 類(也稱爲 invoker)負責發起請求。此類必須有一個用於存儲對命令對象的引用的字段。發送方觸發該命令,而不是直接向接收方發送請求。請注意,發送方不負責創建命令對象。通常,它通過構造函數從客戶端獲取預先創建的命令。

  2. 2. Command 接口通常只聲明一個執行命令的方法。

  3. 3. 在接收對象上執行方法所需的參數可以在具體命令中聲明爲字段。通過只允許通過構造函數初始化這些字段,可以使命令對象不可變。

  4. 4. Receiver 類包含一些業務邏輯。幾乎任何物體都可以充當接收器。大多數命令只處理如何將請求傳遞給接收方的細節,而接收方本身則執行實際工作。

  5. 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()

適用性

如何實現

  1. 1. 使用單個執行方法 decompose 命令接口。

  2. 2. 開始將請求提取到實現命令接口的具體命令類中。每個類都必須有一組字段,用於存儲請求參數沿着對實際接收器對象的引用。所有這些值都必須通過命令的構造函數初始化。

  3. 3. 確定將充當代理的類。將用於存儲命令的字段添加到這些類中。發送者應僅通過命令接口與其命令進行通信。發送方通常不會自己創建命令對象,而是從客戶端代碼中獲取命令對象。

  4. 4. 更改發送方,使其執行命令,而不是直接向接收方發送請求。

  5. 5. 客戶端應按以下順序初始化對象:

利弊

3L2wYt

與其他模式的關係

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;

文本編輯器:命令和撤消

關鍵點:

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 {
    backupString,
}

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 {
    backupString,
}

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 {
    clipboardString,
    historyVec<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 commandimpl 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