命令模式(學習筆記)

  1. 意圖

  將請求轉換爲一個包含與請求相關的全部信息的獨立對象該轉換讓你能根據不一樣的請求將方法參數化延遲請求執行或將其放入隊列中且能實現可撤銷操做java

  2. 動機

  假如開發一款新的文字編輯器當前的任務是建立一個包含多個按鈕的工具欄並讓每一個按鈕對應編輯器的不一樣操做建立了一個很是簡潔的按鈕它不只可用於生成工具欄上的按鈕還可用於生成各類對話框的通用按鈕。儘管全部按鈕看上去都很類似 但它們能夠完成不一樣的操做 (打開 保存 打印和應用等 問題是在哪裏放置這些按鈕的點擊處理代碼呢 最簡單的解決方案是在使用按鈕的每一個地方都建立大量的子類 這些子類中包含按鈕點擊後必須執行的代碼算法

             

   可是這種方式有嚴重的缺陷首先建立了大量的子類當每次修改基類按鈕都有可能須要修改全部子類的代碼簡單來講GUI 代碼以一種拙劣的方式依賴於業務邏輯中的不穩定代碼(違背了依賴倒置原則)。更棘手的是,複製/粘貼文字等操做可能會在多個地方被調用例如用戶能夠點擊工具欄上小小的 「複製 按鈕或者經過上下文菜單複製一些內容又或者直接使用鍵盤上的 Ctrl+C 。咱們的程序最初只有工具欄所以可使用按鈕子類來實現各類不一樣操做換句話來講複製按鈕Copy­Button子類包含複製文字的代碼是可行的在實現了上下文菜單快捷方式和其餘功能後要麼須要將操做代碼複製進許多個類中要麼須要讓菜單依賴於按鈕然後者是更糟糕的選擇
數據庫

  優秀的軟件設計一般會將變化的部分進行封裝而這每每會致使軟件的分層最多見的例子一層負責用戶圖像界面另外一層負責業務邏輯GUI 層負責在屏幕上渲染美觀的圖形捕獲全部輸入並顯示用戶和程序工做的結果當須要完成一些重要內容時(好比計算月球軌道或撰寫年度報告GUI 層則會將工做委派給業務邏輯底層。在代碼中就是,一個 GUI 對象傳遞一些參數來調用一個業務邏輯對象這個過程一般被描述爲一個對象發送請求給另外一個對象。
網絡

  命令模式建議 GUI 對象不直接提交這些請求 應該將請求的全部細節 (例如調用的對象 方法名稱和參數列表 抽取出來組成命令類 該類中僅包含一個用於觸發請求的方法。GUI 對象觸發命令便可命令對象會自行處理全部細節工做。全部命令實現相同的接口該接口一般只有一個沒有任何參數的執行方法讓你能在不和具體命令類耦合的狀況下使用同一請求發送者執行不一樣命令此外還有額外的好處如今你能在運行時切換鏈接至發送者的命令對象以此改變發送者的行爲編輯器

                   

  3. 適用性

  • 若是須要經過操做來參數化對象,可使用命令模式

  命令模式可將特定的方法調用轉化爲獨立對象。故而能夠將命令做爲方法的參數進行傳遞將命令保存在其餘對象中或者在運行時切換已鏈接的命令等ide

  • 若是想要將操做放入隊列中或者遠程執行操做可以使用命令模式

  同其餘對象同樣命令也能夠實現序列化(序列化的意思是轉化爲字符串從而能方便地寫入文件或數據庫中一段時間後該字符串可被恢復成爲最初的命令對象所以你能夠延遲或計劃命令的執行但其功能遠不止如此使用一樣的方式你還能夠將命令放入隊列記錄命令或者經過網絡發送命令函數

  • 若是你想要實現操做回滾功能 可以使用命令模式  

  儘管有不少方法能夠實現撤銷和恢復功能但命令模式多是其中最經常使用的一種爲了可以回滾操做你須要實現已執行操做的歷史記錄功能命令歷史記錄是一種包含全部已執行命令對象及其相關程序狀態備份的棧結構這種方法有兩個缺點:工具

  首先程序狀態的保存功能並不容易實現由於部分狀態多是私有的你可使用備忘錄模式來在必定程度上解決這個問題this

  其次備份狀態可能會佔用大量內存所以,有時你須要藉助另外一種實現方式命令無需恢復原始狀態而是執行反向操做反向操做也有代價它可能會很難甚至是沒法實現spa

  • 支持修改日誌,這樣在系統崩潰時,修改能夠被重作一遍。在command接口中添加裝載操做和存儲操做,能夠用來保持一個一致的修改日誌。從崩潰中恢復的過程包括從磁盤中從新讀入記錄下的命令並用Execute操做從新執行它們
  • 用構建在原語操做上的高層操做構建一個系統。這樣一種結構在支持事物的信息系統中很常見。一個事務封裝了對數據的一組變更。Command模式提供了對事務進行建模的方法。Command有一個公共接口,使得你能夠用同一種方式調用全部的事務。同時,使用該模式也易於添加新事務以擴展系統

  4. 結構

      

  5. 效果

  1. Command模式將調用操做的對象與知道如何實現該操做的對象解耦(單一職責原則)

  2. 實現撤銷和恢復功能

  3. 實現操做的延遲執行

  4. 能夠將多個命令裝配成一個組合命令。通常來講,組合命令是Composite模式的一個實例

  5. 能夠在不修改客戶端代碼的狀況下,在程序中建立新的命令(開閉原則)  

  6. 代碼實現  

  commands/Command.java: 抽象基礎命令

package command.commands;

import command.editor.Editor;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:05
 */
public abstract class Command {
    public Editor editor;
    private String backup;

    Command(Editor editor) {
        this.editor = editor;
    }

    void backup() {
        backup = editor.textField.getText();
    }

    public void undo() {
        editor.textField.setText(backup);
    }

    public abstract boolean execute();
}

  commands/CopyCommand.java: 將所選文字複製到剪貼板

package command.commands;

import command.editor.Editor;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:06
 */
public class CopyCommand extends Command {
    public CopyCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        editor.clipboard = editor.textField.getSelectedText();
        return false;
    }

}

  commands/PasteCommand.java: 從剪貼板粘貼文字

package command.commands;

import command.editor.Editor;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:06
 */
public class PasteCommand extends Command{
    public PasteCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;

        backup();
        editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
        return true;
    }
}

  commands/CutCommand.java: 將文字剪切到剪貼板

package command.commands;

import command.editor.Editor;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:06
 */
public class CutCommand extends Command{
    public CutCommand(Editor editor) {
        super(editor);
    }

    @Override
    public boolean execute() {
        if (editor.textField.getSelectedText().isEmpty()) return false;

        backup();
        String source = editor.textField.getText();
        editor.clipboard = editor.textField.getSelectedText();
        editor.textField.setText(cutString(source));
        return true;
    }

    private String cutString(String source) {
        String start = source.substring(0, editor.textField.getSelectionStart());
        String end = source.substring(editor.textField.getSelectionEnd());
        return start + end;
    }
}

  commands/CommandHistory.java: 命令歷史

package command.commands;

import java.util.Stack;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:06
 */
public class CommandHistory {
    private Stack<Command> history = new Stack<>();

    public void push(Command c) {
        history.push(c);
    }

    public Command pop() {
        return history.pop();
    }

    public boolean isEmpty() { return history.isEmpty(); }
}

  editor/Editor.java: 文字編輯器的 GUI

package command.editor;

import command.commands.*;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:06
 */
public class Editor {
    public JTextArea textField;
    public String clipboard;
    private CommandHistory history = new CommandHistory();

    public void init() {
        JFrame frame = new JFrame("Text editor (type & use buttons, Luke!)");
        JPanel content = new JPanel();
        frame.setContentPane(content);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
        textField = new JTextArea();
        textField.setLineWrap(true);
        content.add(textField);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        JButton ctrlC = new JButton("Ctrl+C");
        JButton ctrlX = new JButton("Ctrl+X");
        JButton ctrlV = new JButton("Ctrl+V");
        JButton ctrlZ = new JButton("Ctrl+Z");
        Editor editor = this;
        ctrlC.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CopyCommand(editor));
            }
        });
        ctrlX.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new CutCommand(editor));
            }
        });
        ctrlV.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                executeCommand(new PasteCommand(editor));
            }
        });
        ctrlZ.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                undo();
            }
        });
        buttons.add(ctrlC);
        buttons.add(ctrlX);
        buttons.add(ctrlV);
        buttons.add(ctrlZ);
        content.add(buttons);
        frame.setSize(450, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void executeCommand(Command command) {
        if (command.execute()) {
            history.push(command);
        }
    }

    private void undo() {
        if (history.isEmpty()) return;

        Command command = history.pop();
        if (command != null) {
            command.undo();
        }
    }
}

  Demo.java: 客戶端代碼

package command;

import command.editor.Editor;

/**
 * @author GaoMing
 * @date 2021/7/25 - 20:05
 */
public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.init();
    }
}

  運行結果

    

  7. 與其餘模式的關係

  • 責任鏈模式、命令模式、中介者模式和觀察者模式用於處理請求發送者和接收者之間的不一樣鏈接方式:
    - 責任鏈按照順序將請求動態傳遞給一系列的潛在接收者,直至其中一名接收者對請求進行處理
    - 命令在發送者和請求者之間創建單向鏈接
    - 中介者清除了發送者和請求者之間的直接鏈接,強制它們經過一箇中介對象進行間接溝通
    - 觀察者容許接收者動態地訂閱或取消接收請求

  • 能夠同時使用命令和備忘錄模式來實現「撤銷」。在這種狀況下,命令用於對目標對象執行各類不一樣的操做,備忘錄用來保存一條命令執行前該對象的狀態
  • 原型模式可用於保存命令的歷史記錄
  • 能夠將訪問者模式視爲命令模式的增強版本,其對象可對不一樣類的多種對象執行操做
  • 命令和策略模式看上去很像,由於二者都能經過某些行爲來參數化對象。可是,它們的意圖有很是大的不一樣:
    - 可使用命令來將任何操做轉換爲對象。操做的參數將成爲對象的成員變量。你能夠經過轉換來延遲操做的執行、將操做放入隊列、保存歷史命令或者向遠程服務發送命令等
    - 策略一般可用於描述完成某件事的不一樣方式,讓你可以在同一個上下文類中切換算法

  • 責任鏈的管理者可以使用命令模式實現。在這種狀況下,你能夠對由請求表明的同一個上下文對象執行許多不一樣的操做
    還有另一種實現方式,那就是請求自身就是一個命令對象。在這種狀況下,你能夠對由一系列不一樣上下文鏈接而成的鏈執行相同的操做

  8. 已知應用  

  使用示例:命令模式在 Java 代碼中很常見。大部分狀況下,它被用於代替包含行爲的回調函數,此外還被用於對任務進行排序和記錄操做歷史記錄等  如下是在覈心 Java 程序庫中的一些示例:  java.lang.Runnable 的全部實現  javax.swing.Action 的全部實現  識別方法:命令模式能夠經過抽象或接口類型(發送者)中的行爲方法來識別,該類型調用另外一個不一樣的抽象或接口類型(接收者)實現中的方法,該實現則是在建立時由命令模式的實現封裝。

相關文章
相關標籤/搜索