一步一步理解命令模式

TimberLiu發表於2019-05-17

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

命令模式

現在有一個裝修公司,在裝修房子時會安裝一個家用電器的總控制器,例如有電燈、空調、熱水器、電腦等電器,這個控制器上的每一對 ON/OFF 開關就對應了一個具體的裝置,可以對該裝置進行操作。

另外,有些使用者家中可能沒有熱水器,不需要對其進行控制,而有些使用者家中可能還有電視,又需要對電視進行控制。所以,具體對哪些裝置進行控制,需要由使用者自己決定。試想一下,這個系統該如何設計呢?

版本一

我們先來嘗試一下。例如,現在需要對電燈、空調、電腦進行控制,這三個實體類定義如下(注意它們是由不同的廠家製造,其介面不同):

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);
    }
}
// 開啟電燈
// 關閉電燈
複製程式碼

對於上面的這種方式,由於無法預先知道控制器上的槽對應的什麼裝置,所以控制器的實現中使用了大量的型別判斷語句,我們可以看到,這樣的設計很不好。

另外,如果有別的使用者想要控制其他裝置,就需要去修改控制器的程式碼,這明顯不符合開閉原則,並且會造成很大的工作量。

版本二

那該如何進行改進呢?我們想著要是這些裝置的介面可以修改就好了,我們將它們的介面修改成統一的,也就不需要再去一個一個地判斷了。

來看一下它如何實現,我們定義一個家電介面,其中包含開關操作,然後讓不同的家電裝置去實現它。

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 設計模式》

相關文章