命令模式

GitHub代碼

場景描述:我們需要構建一個遙控器程序,它擁有幾個插槽,而這些插槽可以配置不同功能的api,並且擁有一個撤銷按鈕,可以撤銷上一次的操作。
第一構想:使用大段的if else做邏輯判斷,但是這是一種非常糟糕的設計。

在提出改進方案之前,讓我們看一個關於餐廳點餐的示例。
在這裏插入圖片描述
進而引申出命令模式
在這裏插入圖片描述

命令模式將“請求”封裝成對象,以便使用不同的請求、隊列或者日誌參數化其他對象。命令模式也支持可撤銷的操作。

一個命令對象通過在特定接收者上綁定一組動作來封裝一個請求。
我們也可以創建命令的宏,以便一次執行多個命令。
宏:批量處理。將一些命令組織在一起作爲一個單獨命令完成一個特定任務。

這是它的類圖:
在這裏插入圖片描述
讓我們回到之前的遙控器問題,我們嘗試使用命令模式來實現它。
首先新建一個命令接口

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

接着實現一個打開電燈的命令

public class LightOnCommand implements Command {
    Light light;

    public LightOnCommand(Light light){
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

    @Override
    public void undo() {
        light.off();
    }
}

而真正的執行者則是電燈

public class Light {
    String location;

    public Light(String location){
        this.location = location;
    }

    public void on(){
        System.out.println(location + " light is on");
    }

    public void off(){
        System.out.println(location + " light is off");
    }
}

所以我們的遙控器可以這麼設置

public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;

    public RemoteControl(){
        onCommands = new Command[7];
        offCommands = new Command[7];

        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++){
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }

    public void setCommand(int slot, Command onCommand, Command offCommand){
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot){
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    public void offButtonWasPushed(int slot){
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonWasPushed(){
        undoCommand.undo();
    }

    public String toString(){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("\n------- Remote Control -------\n");
        for (int i = 0; i < offCommands.length; i++){
            stringBuffer.append("[slot " + i + "] " + onCommands[i].getClass().getName() + "    " + offCommands[i].getClass().getName() + " \n");
        }
        return stringBuffer.toString();
    }
}

NoCommand對象是一個空對象的例子。當你不想返回一個有意義的對象時,空對象就很有用。客戶也可以將處理null的責任轉移給空對象。

public class NoCommand implements Command {
    @Override
    public void execute() {}

    @Override
    public void undo() {}
}

我們嘗試進行一次測試,看看命令模式的效果

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();

        Light livingRoomLight = new Light("Living Room");
        Light kitchenLight = new Light("Kitchen");
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        GarageDoor garageDoor = new GarageDoor("");
        Stereo stereo = new Stereo("Living Room");

        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
        LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);

        CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
        GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);

        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
        StereoOffWithCDCommand stereoOffWithCD = new StereoOffWithCDCommand(stereo);

//        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
//        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
//        remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
//        remoteControl.setCommand(3, stereoOnWithCD, stereoOffWithCD);
//
//        System.out.println(remoteControl);
//
//        remoteControl.onButtonWasPushed(0);
//        remoteControl.offButtonWasPushed(0);
//        remoteControl.onButtonWasPushed(1);
//        remoteControl.offButtonWasPushed(1);
//        remoteControl.onButtonWasPushed(2);
//        remoteControl.offButtonWasPushed(2);
//        remoteControl.onButtonWasPushed(3);
//        remoteControl.offButtonWasPushed(3);

        //測試undo
        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();

        System.out.println("--------------------------------------------------------");

        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();
    }
}

接着我們嘗試使用宏命令,這需要創建一個特殊的命令類。

public class MacroCommand implements Command {
    Command[] commands;

    public MacroCommand(Command[] commands){
        this.commands = commands;
    }

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

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

我們還是用之前的代碼測試一下

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();

        Light livingRoomLight = new Light("Living Room");
        Light kitchenLight = new Light("Kitchen");
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        GarageDoor garageDoor = new GarageDoor("");
        Stereo stereo = new Stereo("Living Room");

        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);

        CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
        StereoOffWithCDCommand stereoOffWithCD = new StereoOffWithCDCommand(stereo);

        // 測試宏命令
        Command[] partyOn = {livingRoomLightOn, stereoOnWithCD, ceilingFanOn};
        Command[] partyOff = {livingRoomLightOff, stereoOffWithCD, ceilingFanOff};

        MacroCommand partyOnMacro = new MacroCommand(partyOn);
        MacroCommand partyOffMacro = new MacroCommand(partyOff);

        remoteControl.setCommand(0, partyOnMacro, partyOffMacro);

        System.out.println(remoteControl);
        System.out.println("-------Pushing Macro On-------");
        remoteControl.onButtonWasPushed(0);
        System.out.println("-------Pushing Macro Off-------");
        remoteControl.offButtonWasPushed(0);
    }
}

如果我們想要實現多層次的撤銷操作,可以使用一個堆棧記錄操作過程的每一個命令,然後按下撤銷按鈕時,可以從堆棧中取出最上層的命令,然後執行它的undo()方法即可。

其他應用:
工作隊列
命令可以將運算塊打包(一個接收者和一組動作),然後將它傳來傳去,就像是一般的對象一樣。甚至可以在不同的線程種被調用。因此我們可以利用這種特性衍生出一些應用,如:日程安排,線程池,工作隊列等。
工作隊列:你在一端添加命令,然後另一端則是線程。線程進行下面的動作:從隊列中取出一個命令,調用它的execute()方法,等待這個調用完成,然後將此命令對象丟棄,再取出下一個命令。
日誌請求
某些應用需要我們將所有的動作都記錄在日誌中,並能在系統死機之後,重新調用這些動作恢復到之前的狀態。通過新增兩個方法(store()、load()),命令模式就能夠支持這一點。
有許多調用大型數據結構的動作的應用無法在每次改變發生時快速地存儲。通過使用記錄日誌,我們可以將上次檢查點之後地所有操作記錄下來,如果系統出狀況,從檢查點開始應用這些操作。

總結:命令模式的使用場景是:當我們需要將請求方法封裝起來,而不需要知道具體是哪個實體執行了具體的請求時,就可以通過調用者來設置具體的命令實體,進而有具體的執行實體執行具體方法。
比如上述示例中,我們實現的遙控器就是一個調用者,而開燈關燈等命令就是具體的命令實體,在命令實體中包含了具體的執行實體,並通過它來執行具體方法。我們(即客戶)只需使用遙控器(即調用者)的相關彈性方法即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章