1. 程式人生 > >設計模式——命令模式(Command Pattern)

設計模式——命令模式(Command Pattern)

一、命令模式的定義

將“請求”封裝成物件,以便使用不同的請求,佇列或者日誌來引數化其他物件。命令模式也支援可撤銷。

這裡寫圖片描述

命令介面–ICommand

public interface ICommand {
    public void execute();
    public void undo();
}

定義統一的介面,所有的命令類都需要實現該介面。

命令類–ConcreteCommand

public class ConcreteCommand implements ICommand {
    private Reciever reciever;
    public
ConcreteCommand(Reciever receiver) { this.reciever = receiver; } public void execute() { reciever.action(); } public void undo() { reciever.undo(); } }

將某一個具體的“請求”封裝成具體的命令類,實現ICommand介面。命令模式的核心目的是將發出請求的物件和執行請求的物件解耦,被解耦的兩者之間是通過命令物件進行溝通的,命令物件封裝了接受者及其一個或多個動作。比如:開燈命令,可以是LightOnCommand;

命令執行者—Reciever

public class Reciever {
    private String recieverName;
    pubic Reciever(String name) {
        this.recieverName = name;
    }
    public void action() {
        System.out.println("do action...");
    }
    public void undoAction() {
        System.out.println("undo action ..."
); } }

該類是真正執行命令的類,實質上它根本不知道ICommand和Command的存在,所以它只要純粹完成自己的邏輯。比如一個電燈類(Light),就只需要實現開關電燈的邏輯,並暴露出呼叫介面就行了。

呼叫者–Invoker

public class Invoker {
    private ICommand command;
    public Invoker() {}
    public void setCommand(ICommand command) {
        this.command = command;
    }
    // 呼叫建立時就設定好的命令物件
    public invokeCommand() {
        this.command.execute();
    }

    // 動態呼叫通過引數傳過來的命令物件
    public invokeCommand(ICommand command) {
        command.execute();
    }
    public invokeUndo() {
        this.command.undo();
    }
}

命令的呼叫者,它持有一個或者多個命令物件,在某個時間點呼叫命令物件的execute方法,將請求付諸執行。

客戶端–Client

public class Client {
        Receiver reciever = new Reciever();   
        ICommand c = new ConcreteCommand(reciever);    
        Invoker invoker = new Invoker();   
        invoker.setCommand(c);   
        invoker.invokeCommand(); 
}

負責建立具體的命令物件,並設定命令的執行者。

巨集命令

public class MacroCommand implements ICommand {
    ICommands[] commands;
    public MacroCommand(ICommand[] commands) {
        this.commands = commands;
    }

    public void execute() {
        for (int i=0; i<commands.length; i++) {
            commands[i].execute();
        }
    }

    public void undo() {
        for (int i=0; i<commands.length; i++) {
            commands[i].undo();
        }
    }
}

巨集命令的目的是,製造一種新的命令來執行其他一堆命令。這樣這個巨集命令也是一個實現了ICommand的命令物件,因此可以普通的命令物件一樣被執行。

二、一個具體的例子

實現一個控制電燈開關的遙控器,有三個功能:開、關、撤銷操作。

/**
 * 命令真正執行者,對應類圖中的Reciever
 */
public class Light {
    public void onLight() {
        System.out.println("light on...");
    }

    public void offLight() {
        System.out.println("light off...");
    }

    public void undo() {
        System.out.println("light undo...")
    }
}

/**
 * 開燈命令,對應類圖中的ConcreteCommand
 */
public class LightOnCommand implements ICommand{
    private Light light;
    public LightOnCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        light.onLight();
    }
    public void undo() {
        light.undo();
    }
}
/**
 * 關燈命令,對應類圖中的ConcreteCommand
 */
public class LightOffCommand implements ICommand{
    private Light light;
    public LightOffCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        light.offLight();
    }
    public void undo() {
        light.undo();
    }
}

/**
 * 定義一個空命令物件,它是一個空物件,不做任何事情。可能遙控器在出廠時並沒有
 * 設定真正有效的命令物件,就可以用空物件代替,以便後續在設定真正的命令物件。
 */
public class NoCommand implements ICommand {
    public void execute(){}
    public void undo(){}
}

/**
 * 命令呼叫者,對應類圖中的Invoker
 */
public class LightController {
    // 持有多個命令物件
    private ICommand[] commands;
    // 記錄前一個命令物件,用於“撤銷”操作
    private ICommand undoCommand;

    public LightController() {
        // 用於儲存開和關兩個命令物件
        this.commands = new ICommand[2];
        // 初始化
        ICommand noCommand = new NoCommand();
        forint i=0; i<2; i++){
            command[i] = noCommand;
        }
        undoCommand = noCommand;
    }

    public void setCommands(ICommand onCommand, ICommand offCommand) {
        // 第一個元素被設為開命令物件
        commands[0] = onCommand;
        commands[1] = offCommand;
    }
    // 按下開啟電燈的按鈕(請求開啟電燈)
    public void onButtonPushed() {
        commands[0].execute();
        undoCommand = commands[0];
    }
    // 按下打關電燈的按鈕(請求關掉電燈)
    public void offButtonPushed() {
        commands[1].execute();
        undoCommand = commands[1];
    }

    // 按下撤銷按鈕,由對應的命令物件執行其撤銷操作
    pubic void undoButtonPushed() {
        undoCommand.undo();
    }
    // ...其他程式碼
}

/**
 * 建立者,對應類圖中的Client類
 */
public class LightControllerTest {
    public void static main(String[] args) {
        // 建立命令物件及其執行者
        Light light = new Light();
        LightOnCommand onCommand = new LightOnCommand(light);
        LightOffCommand offCommand = new LightOffCommand(light);

        LightController lightController = new LightController();
        lightController.setCommands(onCommand, offCommand);

        // 測試
        lightController.onButtonPushed();
        lightController.offButtonPushed();
        lightController.undoButtonPushed();
    }
}

三、優缺點

優點

  • 將發出請求的物件與執行請求的物件解耦。
  • 呼叫者可以執行通過引數傳過來的命令物件,因而可以在執行時動態執行命令。
  • 命令的傳送者和命令執行者有不同的生命週期。命令傳送了並不是立即執行。
  • 可以使用巨集命令任意組合一些命令,比如實現“一鍵式家居控制”的需求,就可以將各種家電的命令物件組合成一組。
  • 因為將“請求”封裝成了物件,使得命令可以被傳遞,被延時執行。比如,將命令物件傳送到遠端服務進行執行;也可以將命令物件扔到佇列中,由專門的執行命令執行緒進行執行,執行命令的執行緒不需要知道具體命令內容,只要是實現了ICommand介面的物件,它都可以執行。
  • 因為將“請求”封裝成了物件,從而使得可以對請求的執行過程進行一些控制。比如“撤銷”操作可以讓命令執行失敗之後,進行撤銷,可用於事務執行。
  • 可將命令記錄到日誌,系統故障後恢復等。

缺點

  • 每一個命令都封裝為命令物件,導致類太多。
  • 對於這一點,個人感覺不需要如此細的粒度,比如LightOnCommand,LightOffCommand,只要設計一個合適的資料結構,通過資料來表達,就可以省掉很多類,程式碼的靈活和可擴充套件性也不受影響。

四、應用場景

佇列

命令物件扔到佇列中,由專門的執行命令執行緒進行執行,執行命令的執行緒不需要知道具體命令內容,只要是實現了ICommand介面的物件,它都可執行。因為命令模式下請求者和執行者可以有不同的生命週期,所以命令物件在建立很久以後,仍然可以被執行。另外,可以使用執行緒池來執行這些命令,只要這些命令實現Runable介面。

日誌

因為命令物件可以對命令自身進行管理,比如可以輸出命令日誌,所以可以用於“容災”等需要記錄所有命令的應用。在這些應用中,命令物件中新增記錄日誌將命令儲存到磁碟中,一旦系統崩潰或宕機,重啟後可以重新載入命令,再批次執行命令物件的execute方法。比如資料庫事務處理,電子表格等應用。

狀態條

如果假如系統需要按順序執行一系列的命令操作,並且需要了解命令執行進度和狀態,則可以讓每個命令物件都提供一個獲取命令執行狀態的方法,系統呼叫該方法顯示狀態。

需要多級撤銷或重做操作

可以在Invoker中用一個棧來儲存執行過的命令物件,一旦需要撤銷或重做操作,可以通過pop出最近的一個命令物件,並執行其undo方法。

web服務請求處理

將每個web請求封裝成一個命令物件。比如struts框架中,就用到了命令模式: Struts框架中,在模型層都要繼承一個Action介面,並實現execute方法,其實這個Action就是命令類。為什麼Struts會應用命令模式,是因為Struts的核心控制器ActionServlet只有一個,相當於Invoker,而模型層的類會隨著不同的應用有不同的模型類,相當於具體的Command。這樣,就需要在ActionServlet和模型層之間解耦,而命令模式正好解決這個問題。