這篇文章呢,咱們來學習一下命令模式,一樣地咱們會從一個例子入手(對《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 圖以下:
下面總結一下命令模式的優勢:
缺點:
對於線程池(這裏咱們先不考慮線程數小於核心線程數的狀況),咱們將任務(命令)添加到阻塞隊列(工做隊列)的某一端,而後線程從另外一端獲取一個命令,調用它的 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();
}
}
} ···
}
複製代碼
這裏簡單地說了一下,具體線程池的實現,感興趣的小夥伴能夠本身研究一下。