1. 程式人生 > 其它 >設計模式之命令模式

設計模式之命令模式

定義

將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化,對請求排隊或記錄請求日誌,以及支援可撤銷的操作。
可以類比現實生活中我們使用電視遙控器開關機,或者去餐廳吃飯向服務員點餐的過程,使用者不需要知道點的菜是具體哪個廚師做的,
廚師也不需要知道這個菜是哪個使用者點的,命令傳送者和執行者之間解耦。

結構

  • Command,命令介面,定義執行的方法。
  • ConcreteCommand,具體命令,擁有接收者物件,呼叫接收者的功能來完成命令要執行的操作。
  • Receiver,接收者,真正執行命令的物件。
  • Invoker,呼叫者,是請求的傳送者,通常會擁有很多命令物件,並通過訪問命令物件來執行相關請求。
  • Client,客戶端,也可以稱為裝配者,組裝命令物件和接收者,並觸發執行。

以使用者餐廳點餐為例,使用者就是客戶端,服務員就是呼叫者,點餐就是命令,廚師就是接收者。

簡單實現

命令介面

public interface Command {

  void execute();
}

具體命令

public class ConcreteCommand implements Command {

  private Receiver receiver;

  public ConcreteCommand(Receiver receiver) {
    this.receiver = receiver;
  }

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

接收者

/**
 * 命令接收者
 */
public class Receiver {

  public void action() {
    System.out.println("Receiver the command and execute");
  }
}

呼叫者

public class Invoker {

  private Command command;

  public Invoker(Command command) {
    this.command = command;
  }

  public void runCommand() {
    command.execute();
  }
}

客戶端

public class Client {

  public static void main(String[] args) {
    //組裝命令和執行者
    Receiver receiver = new Receiver();
    Command command = new ConcreteCommand(receiver);
    Invoker invoker = new Invoker(command);
    invoker.runCommand();
  }

}

命令的撤銷和恢復

命令模式的關鍵之處就是將請求封裝成物件,也就是命令物件,並定義了統一的執行操作的介面,這個命令物件可以被儲存,轉發,記錄,處理,撤銷等,
整個命令模式都是圍繞這個物件在進行。這裡我們模擬實現一個支援撤銷和恢復的簡單文字編輯器,類似EditPlus或Word的撤銷和恢復功能。

有兩種思路來實現這種撤銷功能

  • 一種是補償式,又稱反操作式,比如被撤銷的操作是新增,撤銷就是刪除。
  • 另一種是儲存恢復式,將操作前的狀態記錄下來,撤銷的時候直接恢復回去就可以了。關於這種方式,我們學習到備忘錄模式時再詳解。

這裡我們使用第一種方式實現撤銷功能。

/**
 * 命令接收者
 */
public class Receiver {

  /**
   * 文字內容
   */
  private String textContent = "";

  /**
   * 文字追加
   */
  public void append(String target) {
    System.out.println("操作前內容:" + textContent);
    textContent = textContent.concat(target);
    System.out.println("操作後內容:" + textContent);
  }

  /**
   * 文字刪除
   */
  public void remove(String target) {
    System.out.println("操作前內容:" + textContent);
    if (textContent.endsWith(target)) {
      textContent = textContent.substring(0, textContent.length() - target.length());
    }
    System.out.println("操作後內容:" + textContent);
  }
}

命令介面

public interface Command {

  /**
   * 命令執行
   */
  void execute();

  /**
   * 命令撤銷
   */
  void undo();

}

文字追加命令

public class AppendCommand implements Command {

  private Receiver receiver;
  private String target;

  public AppendCommand(Receiver receiver, String target) {
    this.receiver = receiver;
    this.target = target;
  }

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

  @Override
  public void undo() {
    receiver.remove(target);
  }
}

文字刪除命令

public class RemoveCommand implements Command {

  private Receiver receiver;
  private String target;

  public RemoveCommand(Receiver receiver, String target) {
    this.receiver = receiver;
    this.target = target;
  }

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

  @Override
  public void undo() {
    receiver.append(target);
  }
}

文字編輯器(呼叫者),內部儲存命令執行的歷史記錄(可撤銷的列表)和撤銷執行的歷史記錄(可恢復的列表),有撤銷才會有恢復,
所以在執行撤銷的時候向可恢復列表新增命令。撤銷和恢復都是最後執行的要先撤銷和恢復,所以使用棧儲存。

import java.util.Stack;

/**
 * 文字編輯器,支援撤銷和恢復
 */
public class TextEditor {

  private Command command;
  //操作的歷史記錄
  private Stack<Command> undoStack = new Stack<>();
  //撤銷的歷史記錄
  private Stack<Command> redoStack = new Stack<>();

  public void setCommand(Command command) {
    this.command = command;
  }

  public void editText() {
    command.execute();
    undoStack.push(command);
  }

  /**
   * 撤銷功能
   */
  public void undoText() {
    if (!undoStack.isEmpty()) {
      Command command = undoStack.pop();
      command.undo();
      redoStack.push(command);
    }
  }

  /**
   * 恢復功能
   */
  public void redoText() {
    if (!redoStack.isEmpty()) {
      Command command = redoStack.pop();
      command.execute();
    }
  }
}

客戶端

public class Client {

  public static void main(String[] args) {
    //組裝命令和執行者
    Receiver receiver = new Receiver();
    TextEditor textEditor = new TextEditor();
    //追加hello
    textEditor.setCommand(new AppendCommand(receiver, "hello"));
    textEditor.editText();
    //追加world
    textEditor.setCommand(new AppendCommand(receiver, "world"));
    textEditor.editText();
    //刪除orld
    textEditor.setCommand(new RemoveCommand(receiver, "orld"));
    textEditor.editText();
    //撤銷
    textEditor.undoText();
    //撤銷
    textEditor.undoText();
    //恢復
    textEditor.redoText();
  }

}

輸出為

操作前內容:
操作後內容:hello
操作前內容:hello
操作後內容:helloworld
操作前內容:helloworld
操作後內容:hellow
操作前內容:hellow
操作後內容:helloworld
操作前內容:helloworld
操作後內容:hello
操作前內容:hello
操作後內容:helloworld

結果符合預期

巨集命令

簡單來說就是包含多個命令的命令,在餐廳點餐中,使用者所有點的菜組成的選單就是一個巨集命令,每一道菜都是一個命令,類似於組合模式,這裡就不實現了。

簡化的命令模式

在實際開發中,我們可以簡化命令模式,將具體命令和接收者合二為一,呼叫者也不需要持有命令物件了,直接通過方法引數傳遞過來,
將具體命令類的實現改成匿名內部類實現,這個時候的命令模式基本上等同於java回撥機制的實現。

public interface Command {

  void execute();
}
/**
 * 呼叫者
 */
public class Invoker {

  public void runCommand(Command command) {
    command.execute();
  }
}
public class Client {

  public static void main(String[] args) {
    Invoker invoker = new Invoker();
    invoker.runCommand(() -> {
      System.out.println("Concrete Command execute");
    });
  }

}

命令模式在JDK的實現

jdk中執行緒池ThreadPoolExecutor的實現

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Client {

  public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>());
    executor.execute(() -> {
      System.out.println("test execute");
    });
    executor.shutdown();
  }

}

ThreadPoolExecutor可以看做Invoker呼叫者,Runnable就是Command介面,將來不及執行的請求放到佇列中,這可以看做命令模式中的請求佇列化操作。

總結

優點

  1. 更鬆散的耦合,命令的發起者和命令的執行者相互之間不需要知道對方。
  2. 更動態的控制,可以動態的對命令物件進行佇列化,日誌化,撤銷等操作。
  3. 很容易組成複合命令,也就是巨集命令,使系統操作更簡單,功能更強大。
  4. 更好的擴充套件性,很容易增加新的命令物件。

缺點

  1. 可能會導致建立過多的具體命令類。

本質

命令模式的本質是封裝請求,封裝為請求就可以進行撤銷,佇列化,巨集命令等處理了。

使用場景

  1. 如果需要在不同的時刻排隊執行請求,可以使用命令模式,將請求封裝成命令物件並佇列化。
  2. 如果需要支援撤銷操作。

參考

大戰設計模式【8】—— 命令模式
設計模式的征途—19.命令(Command)模式
設計模式(十五)——命令模式(Spring框架的JdbcTemplate原始碼分析)
命令模式(詳解版)
設計模式——命令模式
研磨設計模式-書籍