1. 意圖
將請求轉換為一個包含與請求相關的所有資訊的獨立物件,該轉換讓你能根據不同的請求將方法引數化、延遲請求執行或將其放入佇列中,且能實作可撤銷操作
2. 動機
假如開發一款新的文字編輯器,當前的任務是創建一個包含多個按鈕的工具列,并讓每個按鈕對應編輯器的不同操作,創建了一個非常簡潔的按鈕類,它不僅可用于生成工具列上的按鈕,還可用于生成各種對話框的通用按鈕,盡管所有按鈕看上去都很相似, 但它們可以完成不同的操作 (打開、 保存、 列印和應用等), 問題是在哪里放置這些按鈕的點擊處理代碼呢? 最簡單的解決方案是在使用按鈕的每個地方都創建大量的子類, 這些子類中包含按鈕點擊后必須執行的代碼,
但是這種方式有嚴重的缺陷,首先,創建了大量的子類,當每次修改基類按鈕時,都有可能需要修改所有子類的代碼,簡單來說,GUI 代碼以一種拙劣的方式依賴于業務邏輯中的不穩定代碼(違背了依賴倒置原則),更棘手的是,復制/粘貼文字等操作可能會在多個地方被呼叫,例如用戶可以點擊工具列上小小的 “復制” 按鈕,或者通過背景關系選單復制一些內容,又或者直接使用鍵盤上的 Ctrl+C ,我們的程式最初只有工具列,因此可以使用按鈕子類來實作各種不同操作,換句話來說,復制按鈕Copy-Button子類包含復制文字的代碼是可行的,在實作了背景關系選單、快捷方式和其他功能后,要么需要將操作代碼復制進許多個類中,要么需要讓選單依賴于按鈕,而后者是更糟糕的選擇
優秀的軟體設計通常會將變化的部分進行封裝,而這往往會導致軟體的分層,最常見的例子:一層負責用戶影像界面;另一層負責業務邏輯,GUI 層負責在螢屏上渲染美觀的圖形,捕獲所有輸入并顯示用戶和程式作業的結果,當需要完成一些重要內容時(比如計算月球軌道或撰寫年度報告),GUI 層則會將作業委派給業務邏輯底層,在代碼中就是,一個 GUI 物件傳遞一些引數來呼叫一個業務邏輯物件,這個程序通常被描述為一個物件發送請求給另一個物件,
命令模式建議 GUI 物件不直接提交這些請求, 應該將請求的所有細節 (例如呼叫的物件、 方法名稱和引數串列) 抽取出來組成命令類, 該類中僅包含一個用于觸發請求的方法,GUI 物件觸發命令即可,命令物件會自行處理所有細節作業,所有命令實作相同的介面,該介面通常只有一個沒有任何引數的執行方法,讓你能在不和具體命令類耦合的情況下使用同一請求發送者執行不同命令,此外還有額外的好處,現在你能在運行時切換連接至發送者的命令物件,以此改變發送者的行為,
3. 適用性
- 如果需要通過操作來引數化物件,可以使用命令模式
命令模式可將特定的方法呼叫轉化為獨立物件,故而可以將命令作為方法的引數進行傳遞、將命令保存在其他物件中,或者在運行時切換已連接的命令等,
- 如果想要將操作放入佇列中或者遠程執行操作,可使用命令模式
同其他物件一樣,命令也可以實作序列化(序列化的意思是轉化為字串),從而能方便地寫入檔案或資料庫中,一段時間后,該字串可被恢復成為最初的命令物件,因此,你可以延遲或計劃命令的執行,但其功能遠不止如此!使用同樣的方式,你還可以將命令放入佇列、記錄命令或者通過網路發送命令
- 如果你想要實作操作回滾功能, 可使用命令模式
盡管有很多方法可以實作撤銷和恢復功能,但命令模式可能是其中最常用的一種,為了能夠回滾操作,你需要實作已執行操作的歷史記錄功能,命令歷史記錄是一種包含所有已執行命令物件及其相關程式狀態備份的堆疊結構,這種方法有兩個缺點:
首先,程式狀態的保存功能并不容易實作,因為部分狀態可能是私有的,你可以使用備忘錄模式來在一定程度上解決這個問題,
其次,備份狀態可能會占用大量記憶體,因此,有時你需要借助另一種實作方式:命令無需恢復原始狀態,而是執行反向操作,反向操作也有代價:它可能會很難甚至是無法實作
- 支持修改日志,這樣在系統崩潰時,修改可以被重做一遍,在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 的所有實作
識別方法:命令模式可以通過抽象或介面型別(發送者)中的行為方法來識別,該型別呼叫另一個不同的抽象或介面型別(接收者)實作中的方法,該實作則是在創建時由命令模式的實作封裝,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/308448.html
標籤:設計模式
上一篇:中介者模式(學習筆記)
下一篇:備忘錄模式(學習筆記)
