一步一步理解命令模式

這篇文章呢,咱們來學習一下命令模式,一樣地咱們會從一個例子入手(對《Head First 設計模式》這本書上的例子進行了稍微地修改),經過三個版本的迭代演進,讓咱們能更好地理解命令模式。java

命令模式

如今有一個裝修公司,在裝修房子時會安裝一個家用電器的總控制器,例若有電燈、空調、熱水器、電腦等電器,這個控制器上的每一對 ON/OFF 開關就對應了一個具體的設備,能夠對該設備進行操做。設計模式

另外,有些用戶家中可能沒有熱水器,不須要對其進行控制,而有些用戶家中可能還有電視,又須要對電視進行控制。因此,具體對哪些設備進行控制,須要由用戶本身決定。試想一下,這個系統該如何設計呢?數組

版本一

咱們先來嘗試一下。例如,如今須要對電燈、空調、電腦進行控制,這三個實體類定義以下(注意它們是由不一樣的廠家製造,其接口不一樣):ide

public class Lamp {
    // 接口不一樣,也就是開關的方法不一樣
    public void turnOn() {
        System.out.println("打開電燈");
    }
    public void turnOff() {
        System.out.println("關閉電燈");
    }
}

public class AirConditioner {
    public void on() {
        System.out.println("打開空調");
    }
    public void off() {
        System.out.println("關閉空調");
    }
}

public class Computer {
    public void powerOn() {
        System.out.println("打開電腦");
    }
    public void powerOff() {
        System.out.println("關閉電腦");
    }
}
複製代碼

對於控制器呢,因爲咱們事先不知道具體的槽上,對應的是什麼設備。因此,咱們只能一個一個地進行判斷,而後才能執行開關操做。學習

public class SimpleController1 {

    // Object 類型的數組
    private Object[] control = new Object[3];

    public void setControlSlot(int slot, Object controller) {
        control[slot - 1] = controller;
    }

    // 使用 instanceOf 判斷類型
    public void onButtonWasPressed(int slot) {
        if (control[slot - 1] instanceof Lamp) {
            Lamp lamp = (Lamp) control[slot - 1];
            lamp.turnOn();
        } else if (control[slot - 1] instanceof AirConditioner) {
            AirConditioner airConditioner = (AirConditioner) control[slot - 1];
            airConditioner.on();
        } else if (control[slot - 1] instanceof Computer) {
            Computer computer = (Computer) control[slot - 1];
            computer.powerOn();
        }
    }

    public void offButtonWasPushed(int slot) {
        if (control[slot - 1] instanceof Lamp) {
            Lamp lamp = (Lamp) control[slot - 1];
            lamp.turnOff();
        } else if (control[slot - 1] instanceof AirConditioner) {
            AirConditioner airConditioner = (AirConditioner) control[slot - 1];
            airConditioner.off();
        } else if (control[slot - 1] instanceof Computer) {
            Computer computer = (Computer) control[slot - 1];
            computer.powerOff();
        }
    }
}
複製代碼

下面寫個類來測試一下:測試

public class Test {
    public static void main(String[] args) {
        // 三種家電
        Lamp lamp = new Lamp();
        AirConditioner airConditioner = new AirConditioner();
        Computer computer = new Computer();

        // 設置到相應的控制槽上
        SimpleController1 simpleController1 = new SimpleController1();
        simpleController1.setControlSlot(1, lamp);
        simpleController1.setControlSlot(2, airConditioner);
        simpleController1.setControlSlot(3, computer);

        // 對 1 號槽對應的設備進行開關操做
        simpleController1.onButtonWasPressed(1);
        simpleController1.offButtonWasPushed(1);
    }
}
// 打開電燈
// 關閉電燈
複製代碼

對於上面的這種方式,因爲沒法預先知道控制器上的槽對應的什麼設備,因此控制器的實現中使用了大量的類型判斷語句,咱們能夠看到,這樣的設計很很差。this

另外,若是有別的用戶想要控制其餘設備,就須要去修改控制器的代碼,這明顯不符合開閉原則,而且會形成很大的工做量。spa

版本二

那該如何進行改進呢?咱們想着要是這些設備的接口能夠修改就行了,咱們將它們的接口修改爲統一的,也就不須要再去一個一個地判斷了。線程

來看一下它如何實現,咱們定義一個家電接口,其中包含開關操做,而後讓不一樣的家電設備去實現它。設計

public interface HomeAppliance {
    void on();
    void off();
}

public class Lamp implements HomeAppliance {
    @Override
    public void on() {
        System.out.println("打開電燈");
    }
    @Override
    public void off() {
        System.out.println("關閉電燈");
    }
}

public class AirConditioner implements HomeAppliance {
    @Override
    public void on() {
        System.out.println("打開空調");
    }
    @Override
    public void off() {
        System.out.println("關閉空調");
    }

}

public class Computer implements HomeAppliance {
    @Override
    public void on() {
        System.out.println("打開電腦");
    }
    @Override
    public void off() {
        System.out.println("關閉電腦");
    }
}
複製代碼

如此,控制器就能夠這樣設計:

public class SimpleController2 {

    // 三種家電,統一的接口
    private HomeAppliance[] control = new HomeAppliance[3];

    public void setControlSlot(int slot, HomeAppliance controller) {
        control[slot - 1] = controller;
    }

    // 不須要再進行判斷
    public void onButtonWasPressed(int slot) {
        control[slot - 1].on();
    }
    public void offButtonWasPushed(int slot) {
        control[slot - 1].off();
    }
}
複製代碼

下面寫段代碼來測試一下:

public class Test {

    public static void main(String[] args) {
        HomeAppliance lamp = new Lamp();
        HomeAppliance airConditioner = new AirConditioner();
        HomeAppliance computer = new Computer();

        SimpleController2 simpleController2 = new SimpleController2();
        simpleController2.setControlSlot(1, lamp);
        simpleController2.setControlSlot(2, airConditioner);
        simpleController2.setControlSlot(3, computer);

        simpleController2.onButtonWasPressed(1);
        simpleController2.offButtonWasPushed(1);
    }
}
複製代碼

能夠看到,咱們不須要再寫大量的類型判斷語句,而且有用戶想要控制別的設備時,只須要讓該設備實現 HomeAppliance 接口,就能夠了。

但理想很豐滿,顯示很苦幹。惋惜的是這些家電設備的接口從出廠時就已經固定了,沒法再改變,這種方式只是看起來不錯,咱們還須要另尋出路。

版本三

咱們繼續進行改進。那咱們可否將這些設備包裝一下,讓其對外提供統一的開關方法,如此控制器就不須要去判斷是什麼類型,而是隻管去調用包裝後的開關方法就行了。

也就是說從新定義一個統一的接口,它包含了開關操做的方法,而後讓不一樣的設備,都建立一個與它本身對應的類,用來操做它自己。

對於三個實體類,咱們仍然使用第一次嘗試時使用的類。而這個統一的接口能夠這樣定義:

public interface OnOff {
    void on();
    void off();
}
複製代碼

而後,讓不一樣的設備,都建立一個與它本身對應的類,其內部封裝了它本身。在對外提供的統一方法 on/off 實現中,再去調用本身的開關方法:

public class LampOnOff implements OnOff {

    private Lamp lamp;
    
    public Lamp_OnOff(Lamp lamp) {
        this.lamp = lamp;
    }
    @Override
    public void on() {  
        lamp.turnOn();
    }
    @Override
    public void off() {
        lamp.turnOff();
    }
}

public class AirConditionerOnOff implements OnOff {

    private AirConditioner airConditioner;
    
    public AirConditioner_OnOff(AirConditioner airConditioner) {
        this.airConditioner = airConditioner;
    }
    @Override
    public void on() {
        airConditioner.on();
    }
    @Override
    public void off() {
        airConditioner.off();
    }
}

public class ComputerOnOff implements OnOff {

    private Computer computer;
    
    public Computer_OnOff(Computer computer) {
        this.computer = computer;
    }
    @Override
    public void on() {
        computer.powerOn();
    }
    @Override
    public void off() {
        computer.powerOff();
    }
}
複製代碼

這時控制器就能夠這樣寫,和版本 2 很相似:

public class SimpleController3 {

    private OnOff[] onOff = new OnOff[3];

    public void setControlSlot(int slot, OnOff controller) {
        onOff[slot - 1] = controller;
    }

    public void onButtonWasPressed(int slot) {
        onOff[slot - 1].on();
    }
    public void offButtonWasPushed(int slot) {
        onOff[slot - 1].off();
    }
}
複製代碼

下面寫段代碼來測試一下:

public class Test {

    public static void main(String[] args) {
        Lamp lamp = new Lamp();
        AirConditioner airConditioner = new AirConditioner();
        Computer computer = new Computer();

        // 三種設備封裝成統一的接口
        // 也就是三種命令對象
        OnOff lampOnOff = new LampOnOff(lamp);
        OnOff airConditionerOnOff = new AirConditionerOnOff(airConditioner);
        OnOff computerOnOff = new ComputerOnOff(computer);

        SimpleController3 simpleController3 = new SimpleController3();
        simpleController3.setControlSlot(1, lampOnOff);
        simpleController3.setControlSlot(2, airConditionerOnOff);
        simpleController3.setControlSlot(3, computerOnOff);

        simpleController3.onButtonWasPressed(1);
        simpleController3.offButtonWasPushed(1);
    }
}
複製代碼

上面這種作法呢,既沒有了大量的判斷語句,並且用戶想要控制其餘設備時,只須要建立一個實現 OnOff 接口的類,在這個類的 on、off 方法中,調用設備的具體實現便可。

命令模式概述

其實上面的版本三就是命令模式,咱們這就來看一下在 《Head First 設計模式》中對它的定義:它將「請求」封裝成命令對象,以便使用不一樣的請求、隊列或者日誌來參數化其餘對象。命令模式也支持可撤銷操做。

對於這個定義如何理解呢?咱們以上面的例子來講明。

在接收者(電燈)上綁定一組開關動做(turnOn/turnOff 方法)就是請求,而後將請求封裝成一個命令對象(OnOff 對象),它對外只暴露 on/off 方法。

當命令對象(OnOff 對象)的 on/off 方法被調用時,接收者(電燈)就會執行相應的動做(turnOn/turnOff 方法)。對於外界來講,其餘對象不知道究竟哪一個接收者執行了動做,而是隻知道調用了命令對象的 on/off 方法。

在將請求封裝成命令對象後,就能夠用命令來參數化其餘對象,這裏就是控制器的插槽(OnOff[])用不用的命令(OnOff 對象)當參數。

它的 UML 圖以下:

  • 這裏將 SimpleController3 稱爲調用者,它會持有一個或一組命令,並在某個時間調用命令對象的 on/off 方法,執行請求。
  • 這裏將 Lamp 稱爲接收者,它知道如何進行具體的工做。
  • 而調用者調用 on/off 發出請求,而後由 ConcreteCommand 來調用接收者的一個或多個動做。

下面總結一下命令模式的優勢:

  • 下降了調用者和請求接收者的耦合度,使得調用者和請求接收者之間不須要直接交互。
  • 在擴展新的命令時很是容易,只須要實現抽象命令的接口便可。

缺點:

  • 命令的擴展會致使系統含有太多的類,增長了系統的複雜度。

命令模式的具體實踐

JDK#線程池

對於線程池(這裏咱們先不考慮線程數小於核心線程數的狀況),咱們將任務(命令)添加到阻塞隊列(工做隊列)的某一端,而後線程從另外一端獲取一個命令,調用它的 run 方法執行,等待這個調用完成後,再取出下一個命令,繼續執行。

命令(任務)接口的定義以下。而具體的任務由咱們本身實現:

public interface Runnable {
    public abstract void run();
}
複製代碼

在線程池 ThreadPoolExecutor 中有一個阻塞隊列,用於存聽任務,它的部分源碼以下:

public class ThreadPoolExecutor extends AbstractExecutorService {

    // 存放命令
    private final BlockingQueue<Runnable> workQueue;

    // 注意:這裏與上面說的例子中 execute 方法不一樣
    public void execute(Runnable command) {
        ···
        // 線程數大於核心線程數,將命令加入到阻塞隊列
        if (isRunning(c) && workQueue.offer(command)) {
            ···
            // 建立 worker
            addWorker(null, false);
        }
        ···
    }
}
複製代碼

在調用 ThreadPoolExecutor 的 execute 方法時,會將實現命令接口的任務添加到阻塞隊列中。

最終線程在執行 Worker 的 run 方法時,又會調用外部的 runWorker 方法,它會循環從阻塞隊列中一個一個地獲取命令對象,而後調用命令對象的 run 方法執行,一旦完成後,就會再去處理下一個命令對象:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();
    try {
        // 循環調用 getTask 獲取命令對象
        while (task != null || (task = getTask()) != null) {
            w.lock();
            try {
                try {
                    // 調用命令對象的 run 方法執行
                    task.run();
                } ···
            } finally {
                task = null;
                w.unlock();
            }
        }
    } ···
}
複製代碼

這裏簡單地說了一下,具體線程池的實現,感興趣的小夥伴能夠本身研究一下。

參考資料

  • 《Head First 設計模式》