多執行緒場景設計利器:分離方法的呼叫和執行——命令模式總結
前言
個人感覺, 該模式主要還是在多執行緒程式的設計中比較常用 ,尤其是一些非同步任務執行的過程。但是本文還是打算先在單執行緒程式裡總結它的用法,至於多執行緒環境中命令模式的用法,還是想在多執行緒的設計模式裡重點總結。
實現思路
其實思路很簡單,就是把方法的請求呼叫和具體執行過程分開,讓客戶端不知道該請求是如何、何時執行的。那麼如何分開呢?
其實沒什麼複雜的,就是使用 OO 思想,把對方法的請求封裝為物件即可,然後在設計一個請求的接受者物件,當然還要有一個請求的傳送者物件,請求本身也是一個物件。最後,請求要如何執行呢?
故,除了請求物件,請求傳送者,請求接受者,還要一個請求執行者——這裡可以看成是客戶端,而請求(其實叫命令、或者請求都是一樣的意思,後文就用請求這個術語)最好設計為抽象的(或者介面)。
也可得知,命令模式是物件的行為型的設計模式。
簡單的命令模式
模擬場景:線上教育平臺售賣一些培訓的視訊課程,規定必須付費後才能觀看,故管理員需要有開放課程觀看和關閉課程觀看許可權的操作
首先需要一個抽象的命令(請求)介面
public interface ICommand { // 抽象的命令(請求)介面 void execute(); }
然後設計一個課程類——Lesson,它代表課程本身,也是命令(請求)的接受者,因為是對課程這個實體下命令
public class Lesson { // 代表課程本身,也是命令(請求)的接受者,因為是對課程這個實體下命令 private String name; public Lesson(String name) { this.name = name; } public void openLesson() { System.out.println("可以觀看課程:" + name); } public void closeLesson() { System.out.println("不可以觀看課程:" + name); } }
下面是兩個具體的命令類,分別實現命令介面,裡面是有聚合關係,把課程 Lesson 的引用聚合到命令類,哪一個命令要對哪一個實體,不能寫錯,比如關閉對關閉。
public class CloseCommand implements ICommand { private Lesson lesson; public CloseCommand(Lesson lesson) { this.lesson = lesson; } @Override public void execute() { this.lesson.closeLesson(); } } ////////////////////////////////////////////////////// public class OpenCommand implements ICommand { private Lesson lesson; public OpenCommand(Lesson lesson) { this.lesson = lesson; } @Override public void execute() { this.lesson.openLesson(); } }
設計一個管理員類,作為命令(請求)的呼叫者,用來發出請求(命令),而命令的實際執行,交給了命令(請求)的接受者——Lesson
public class Admin2 { private ICommand commond; public void setCommond(ICommand commond) { this.commond = commond; } public void executeCommond() { this.commond.execute(); } }
客戶端
Lesson lesson1 = new Lesson("c++"); // 請求(命令)的接受者 CloseCommand closeCommand1 = new CloseCommand(lesson1); // 命令封裝為物件 OpenCommand openCommand1 = new OpenCommand(lesson1); Admin2 admin2 = new Admin2(); // 請求(命令)的呼叫者:用來發出請求 admin2.setCommond(openCommand1); // 將命令傳給呼叫者 admin2.executeCommond(); // 發出請求(命令),但是admin 並不知道這個請求(命令)發給了誰,是誰在執行這個請求(命令) admin2.setCommond(closeCommand1); admin2.executeCommond();
如上就實現了請求呼叫和具體執行的分離(解耦)
一次執行多個命令
下面是一次執行多個命令的寫法,也可以作為巨集命令的實現
命令介面和具體命令都不變,admin 變化如下:
public class Admin { private List<ICommand> commondList = new ArrayList<>(); // 使用 ArrayList 還能保證命令的順序執行 public void addCommond(ICommand commond) { commondList.add(commond); } public void executeCommond() { for (ICommand commond : commondList) { commond.execute(); } commondList.clear(); } }
當然這裡用棧等資料結構去包裝命令也是可以的
Lesson lesson = new Lesson("java"); // 請求(命令)的接受者 CloseCommand closeCommand = new CloseCommand(lesson); // 命令 OpenCommand openCommand = new OpenCommand(lesson); Admin admin = new Admin(); // 請求(命令)的呼叫者:用來發出請求 admin.addCommond(openCommand); // 將命令傳給呼叫者 admin.addCommond(closeCommand); admin.executeCommond();
引申:空型別模式
再比如,使用靜態陣列去包裝命令,這裡引申一個空型別模式,就是說有一個類,這個類什麼都不做,就是佔位或者初始化用的,代替 null 型別。
下面舉一個例子,設計一個控制器,控制電燈的開關,閃爍,變暗,變亮等操作
public interface ICommand2 { void execute(); // 命令介面 } ////////////////////////////////// public class LightOffCommand implements ICommand2 { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void execute() { this.light.off(); } } ////////////////////////////////// public class LightOnCommand implements ICommand2 { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { this.light.on(); this.light.zoomin(); this.light.blink(); } } ////////////////////////////////// public class EmptyCommand implements ICommand2 { // 空型別模式的體現 @Override public void execute() { System.out.println("什麼都不做"); } } ////////////////////////////////// public class Light { public Light() { } public void on() { System.out.println("電燈開啟"); } public void off() { System.out.println("電燈關閉"); } public void zoomin() { System.out.println("燈光變強"); } public void zoomout() { System.out.println("燈光變弱"); } public void blink() { System.out.println("燈光閃爍"); } public void noBlink() { System.out.println("燈光停止閃爍"); } }
下面是一個控制器類,setCommand 方法可以設定某個命令和某個操作的對應關係,初始化時,使用空型別模式
public class MainController { private ICommand2[] onCommands; private ICommand2[] offCommands; public MainController() { this.onCommands = new ICommand2[3]; this.offCommands = new ICommand2[2]; ICommand2 emptyCommand = new EmptyCommand(); for (int i = 0; i < 3; i++) { this.onCommands[i] = emptyCommand; } for (int i = 0; i < 2; i++) { this.offCommands[i] = emptyCommand; } } public void setCommand(int idx, ICommand2 onCommand, ICommand2 offCommand) { this.onCommands[idx] = onCommand; this.offCommands[idx] = offCommand; } public void executeOnCommand(int idx) { this.onCommands[idx].execute(); } public void executeOffCommand(int idx) { this.offCommands[idx].execute(); } }
客戶端
MainController mainController = new MainController(); Light roomLight = new Light(); Light doorLight = new Light(); LightOnCommand roomLightOnCommand = new LightOnCommand(roomLight); LightOffCommand roomLightOffCommand = new LightOffCommand(roomLight); LightOnCommand doorLightOnCommand = new LightOnCommand(doorLight); LightOffCommand doorLightOffCommand = new LightOffCommand(doorLight); mainController.setCommand(0, roomLightOnCommand, roomLightOffCommand); mainController.setCommand(1, doorLightOnCommand, doorLightOffCommand); mainController.executeOnCommand(0); mainController.executeOffCommand(0); mainController.executeOnCommand(1); mainController.executeOffCommand(1); mainController.executeOnCommand(2);
命令模式在單執行緒環境下的優點(使用場景)
通過封裝對方法的請求呼叫和方法執行過程,並將其分離,也就是所謂的完全解耦了。
故可以對方法的呼叫執行實現一些額外操作,比如記錄日誌,撤銷某個方法的請求呼叫,或者實現一次請求,N 次執行某個方法等。
在架構上,可以讓程式易於擴充套件新的請求(命令)。
命令模式在多執行緒程式中的優點
這樣做,在多執行緒環境下的好處是:
1、 避免演算法(策略)模組執行緩慢拖累呼叫方——抽象了需要等待的操作
2、控制執行順序,因為請求呼叫和具體執行分離,故執行順序和呼叫順序沒有關係
3、可以輕鬆實現請求的取消,或者反覆執行某個請求
4、請求呼叫和具體執行分離後,進一步把負責呼叫的機器和負責執行的機器分開,可以基於網路,實現分散式程式
命令的撤銷實現
前面,無論在什麼環境下,都提到了能撤銷命令(請求),故命令模式經常和備忘錄模式搭配使用。參考:儲存快照和撤銷功能的實現方案——備忘錄模式總結 。
這裡舉一個很簡單的例子,還是電燈開關的例子
public interface ICommand3 { void execute(); void undo(); // 和 execute 執行相反的操作 } ////////////////////////////////// public class EmptyCommand implements ICommand3 { @Override public void execute() { System.out.println("什麼都不做"); } @Override public void undo() { System.out.println("什麼都不做"); } } ///////////////////////////////// public class LightOnCommand implements ICommand3 { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { this.light.on(); this.light.zoomin(); this.light.blink(); } @Override public void undo() { this.light.noBlink(); this.light.zoomout(); this.light.off(); } } /////////////////////////////////// public class LightOffCommand implements ICommand3 { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void execute() { this.light.off(); } @Override public void undo() { this.light.on(); } }
控制器也要變化,初始化命令的同時,也要初始化 undo 命令
public class MainController { private ICommand3[] onCommands; private ICommand3[] offCommands; private ICommand3 undoCommand; // 記錄上一個命令 public MainController() { this.onCommands = new ICommand3[3]; this.offCommands = new ICommand3[2]; ICommand3 emptyCommand = new EmptyCommand(); for (int i = 0; i < 3; i++) { this.onCommands[i] = emptyCommand; } for (int i = 0; i < 2; i++) { this.offCommands[i] = emptyCommand; } this.undoCommand = emptyCommand; // 初始化 undo 命令 } public void setCommand(int idx, ICommand3 onCommand, ICommand3 offCommand) { this.onCommands[idx] = onCommand; this.offCommands[idx] = offCommand; } public void executeOnCommand(int idx) { this.onCommands[idx].execute(); this.undoCommand = this.onCommands[idx]; } public void executeOffCommand(int idx) { this.offCommands[idx].execute(); this.undoCommand = this.offCommands[idx]; } public void undoCommand() { this.undoCommand.undo(); } }
客戶端
MainController mainController = new MainController(); Light roomLight = new Light(); LightOffCommand offCommand = new LightOffCommand(roomLight); LightOnCommand onCommand = new LightOnCommand(roomLight); mainController.setCommand(0, onCommand, offCommand); mainController.executeOnCommand(0); System.out.println(); mainController.executeOffCommand(0); System.out.println(); mainController.undoCommand(); System.out.println(); mainController.executeOffCommand(0); System.out.println(); mainController.executeOnCommand(0); System.out.println(); mainController.undoCommand();
列印如下
電燈開啟 燈光變強 燈光閃爍 電燈關閉 電燈開啟 電燈關閉 電燈開啟 燈光變強 燈光閃爍 燈光停止閃爍 燈光變弱 電燈關閉
命令模式的缺陷
個人覺得,唯一的缺點就是會使得程式複雜性提高,但是我認為微不足道,基礎紮實的 RD 應該無壓力閱讀和使用才對,因為在多執行緒程式裡,該模式大量出現,比如 Netty 等框架就大量使用了該思想。
命令模式和策略模式的區別
策略是不同的演算法做同一件事情。不同的策略之間可以相互替換。比如實現一個支付功能,有微信支付,支付寶支付,各自渠道的支付。。。
命令是不同的命令做不同的事情。對外隱藏了具體的執行細節。比如選單中的複製,移動和壓縮
JDK 中的命令模式
最最常見的就是 lang 包裡的 Runnable 介面,這就是一個命令介面,將對執行緒啟動的請求和具體的執行分離了。實現該介面,也是啟動執行緒推薦的寫法