這篇文章呢,我們來學習一下命令模式,同樣地我們會從一個例子入手(對《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 設計模式》