命令模式(Command Pattern)

begin 2020年12月6日21:08:15

定義

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.——《Design Patterns: Elements of Reusable Object-Oriented Software》

命令模式(Command),將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化,對請求排隊或者記錄請求日誌,以及支援可撤銷的操作。——《設計模式:可復用面向對象軟體的基礎》

圖示

命令模式結構圖:

命令模式結構圖

角色

抽象命令角色(Command):

  • 聲明一個介面包含了執行的操作,如圖中excute方法

具體命令角色(ConcreteCommand):

  • 定義了接收者對象和命令的綁定
  • 實現【抽象命令角色】定義的執行操作,具體實現是:在接收者調用對應的操作

客戶端角色(Client):

  • 創建一個【具體命令角色】,並設置它的接收者

調用者角色(Invoker):

  • 要求命令執行請求,即調用Command.execute方法

接收者角色(receiver):

  • 知道如何執行一個請求相關的操作,真正執行操作的人

程式碼示例

【記事本(notepad)】:通過命令模式實現記事本的複製(copy)、粘貼(paste)的功能

1、記事本打開TEXT文件之後,右鍵菜單(Menu)就是調用者,具體的菜單項目有複製、粘貼等

2、點擊複製、粘貼命令,通過具體命令讓接收者執行該命令,從TEXT複製到剪貼板,或從剪貼板粘貼到TEXT文件。

記事本

具體程式碼如下:

抽象命令角色(Command.java):

public interface Command {
    void execute();
}

具體命令角色(CopyCommand.java、PasteCommand.java):

public class CopyCommand implements Command {
    private Text receiver;

    public CopyCommand(Text receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.copy();
    }
}

public class PasteCommand implements Command {
    private Text receiver;

    public PasteCommand(Text receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.paste();
    }
}

調用者角色(Menu.java):

public class Menu {
    private static Map<String, Command> commandMap = new HashMap<>();

    public void addMenuItem(String menuItemName, Command command) {
        commandMap.put(menuItemName, command);
    }

    public void click(String menuItemName) {
        commandMap.get(menuItemName).execute();
    }

}

接收者(Text.java):

public class Text {
    public void copy() {
        System.out.println("複製成功");
    }
    public void paste() {
        System.out.println("粘貼成功");
    }
}

客戶端角色(Menu.java):

public class Notepad {
    public static void main(String[] args) {
        // 打開文件
        Text receiver = new Text();
        // 初始化右鍵菜單
        Menu menu = new Menu();
        Command copyCommand = new CopyCommand(receiver);
        Command pasteCommand = new PasteCommand(receiver);
        menu.addMenuItem("copy", copyCommand);
        menu.addMenuItem("paste", pasteCommand);
        // 複製命令
        menu.click("copy");
        // 粘貼命令
        menu.click("paste");
    }
}

執行命令結果:
執行命令結果

模式擴展

  • 宏命令:使用組合模式將多個命令組合在一起

實現一個複製並粘貼命令,類似在Intellij IDEA裡面的Windows系統快捷鍵是CTRL + D。

複製粘貼命令

程式碼如下:

public class DuplicateCommand implements Command {
    private List<Command> commandList;
    
    public DuplicateCommand(Text receiver) {
        commandList = new ArrayList<>();
        commandList.add(new CopyCommand(receiver));
        commandList.add(new PasteCommand(receiver));
    }
    
    @Override
    public void execute() {
        for(int i = 0; i < commandList.size(); i ++) {
            commandList.get(i).execute();
        }
    }
}

  • 撤銷操作:使用備忘錄模式保存命令歷史,用來撤銷

我們要記錄TEXT當時的狀態,如TEXT當前有什麼內容

修改後的接收者角色,也是備忘錄模式中的備忘錄角色:

public class TextWithUndo {
    private StringBuilder content;

    public TextWithUndo(String text) {
        this.content = new StringBuilder(text);
    }

    public void paste(String text) {
        content.append(text);
        System.out.println("粘貼成功");
    }

    public TextWithUndo createText() {
        return new TextWithUndo(this.content.toString());
    }

    public String toString() {
        return content.toString();
    }
}

修改後的具體命令角色,備忘錄的發起人角色:

public class PasteCommandWithUndo implements Command {
    private TextWithUndo receiver;
    private UndoCommandCaretaker commandCaretaker;

    public PasteCommandWithUndo(TextWithUndo receiver, UndoCommandCaretaker commandCaretaker) {
        this.receiver = receiver;
        this.commandCaretaker = commandCaretaker;
    }

    @Override
    public void execute() {
        commandCaretaker.addText(this.createUndoReceiver());
        String fromClipboard = " xxx ";
        receiver.paste(fromClipboard);
    }

    public TextWithUndo createUndoReceiver() {
        return new TextWithUndo(this.receiver.toString());
    }

}

備忘錄管理者角色:

public class UndoCommandCaretaker {
    private List<TextWithUndo> receiverList = new ArrayList<>();

    public TextWithUndo getText(int i) {
        return receiverList.get(i);
    }

    public void addText(TextWithUndo text) {
        receiverList.add(text);
    }
}

修改後的客戶端:

public class NotepadWithUndo {
    public static void main(String[] args) {
        // 打開文件
        TextWithUndo receiver = new TextWithUndo("");
        // 初始化右鍵菜單
        Menu menu = new Menu();
        // 備忘錄
        UndoCommandCaretaker commandCaretaker = new UndoCommandCaretaker();
        Command pasteCommand = new PasteCommandWithUndo(receiver, commandCaretaker);
        menu.addMenuItem("paste", pasteCommand);
        // 粘貼命令
        menu.click("paste");

        menu.click("paste");

        System.out.println(receiver.toString()); // 輸出: xxx xxx 

        receiver = commandCaretaker.getText(1);

        System.out.println(receiver.toString()); // 輸出: xxx 
    }
}

這樣子結合備忘錄模式就可以實現撤銷操作

使用場景

  • 請求調用者和請求接收者解耦,使得調用者和接收者不直接交互
  • 在不同的時間執行請求,將請求排隊和執行請求(本文並未給出例子,可自行探究)
  • 支援撤銷(undo)和恢復(redo)操作(本文在模式擴展時有此程式碼樣例)
  • 支援將一組操作組合在一起,即宏命令(上文有樣例)

優點

  • 降低耦合
  • 符合開閉原則,很容易就可以添加新的命令

缺點

  • 可能會產生過多的具體命令類

總結

命令模式的動機是為了消除請求調用者和請求接收者直接的耦合,使得請求調用者無需知道請求調用者。命令模式支援對請求的記錄和排隊、撤銷等操作,也支援一個宏命令,一次執行多個命令。

2021年5月16日14:25:27