本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/command
命令模式(Command Pattern)是一種數(shù)據(jù)驅(qū)動(dòng)的設(shè)計(jì)模式,它屬于行為型模式。請(qǐng)求以命令的形式包裹在對(duì)象中,并傳給調(diào)用對(duì)象。調(diào)用對(duì)象尋找可以處理該命令的合適的處理對(duì)象,并把該命令傳給這個(gè)處理對(duì)象,該處理對(duì)象執(zhí)行命令。
命令模式把發(fā)出命令的責(zé)任和執(zhí)行命令的責(zé)任分開,委派個(gè)不同的對(duì)象。什么情況下需要吧發(fā)出命令的責(zé)任和執(zhí)行命令的責(zé)任分開呢?不好意思,按照我的尿性,又要搬出畫圖的例子了。
例子
我們先考慮發(fā)出命令和執(zhí)行命令耦合起來的情況,看有啥不好。比如畫圖有如下幾個(gè)命令:畫形狀、填充顏色、組合形狀等,每個(gè)操作命令的執(zhí)行由不同的對(duì)象來實(shí)現(xiàn)。當(dāng)用戶在執(zhí)行不同的操作的時(shí)候,畫圖軟件有個(gè)統(tǒng)一的邏輯來處理:
// 以下邏輯位于“命令發(fā)出者”的方法內(nèi):
if(要畫形狀){
形狀處理對(duì)象.畫形狀("圓形");
}
if(要填充顏色){
顏色處理對(duì)象.填充顏色("紅色");
}
if(要組合形狀){
組合處理對(duì)象.組合形狀([]{"紅色圓形","藍(lán)色矩形","黃色三角形");
}
這里有幾個(gè)不太靈活的地方:
- “命令發(fā)出者”要同所有的“命令執(zhí)行者”維持關(guān)系(前者要有后者的所有引用),只要增加一種命令執(zhí)行者,就需要修改命令發(fā)出者的代碼邏輯,顯然不符合“開閉”原則的吧;尤其是命令發(fā)出者如果是不開放修改的(比如位于框架中),就很難保證擴(kuò)展性了;
- 命令執(zhí)行有可能失敗,或者Ctrl+Z回退,這種操作顯然是經(jīng)常遇到的,對(duì)于”命令發(fā)出者“的邏輯復(fù)雜度來說,顯然又是雪上加霜。

我們?cè)賮砜匆幌?,?yīng)用命令模式如何解耦以及解耦后有什么好處。
將發(fā)出命令和執(zhí)行命令解耦,即“發(fā)出命令者”不直接調(diào)用“執(zhí)行命令者”的方法,而是通過一個(gè)“中間媒介”,這個(gè)“中間媒介”就是“命令”。從而由原來的“發(fā)出命令者 - 執(zhí)行命令者”的關(guān)系變成“發(fā)出命令者 - 命令 - 執(zhí)行命令者”。
“發(fā)出命令者”只關(guān)心命令的執(zhí)行,并不關(guān)心由誰(shuí)來執(zhí)行。從而它手中不再握有一系列的“執(zhí)行命令者”的引用,而是只有處理命令(或命令集合,如命令隊(duì)列)的引用。而命令本身有“執(zhí)行命令者”的引用,從而知道找誰(shuí)去完成命令的具體執(zhí)行。

當(dāng)然,要做到完全解耦和保證靈活性,必須針對(duì)“命令”進(jìn)行面向接口的設(shè)計(jì),也就是“發(fā)出命令者”手里的只有抽象類型的命令對(duì)象,才能做到“不care”。這都是設(shè)計(jì)模式的老掉牙的套路了,不再多說。
如此,剛才提到的兩個(gè)不太靈活的問題也就迎刃而解了??创a嘍~
發(fā)出命令者 DrawingApp.java
public class DrawingApp {
private List<Command> commands = new ArrayList<Command>();
public void takeCommand(Command command) {
commands.add(command);
}
public void CommandsDone() {
for (Command command:commands) {
command.doCmd();
}
}
public void undoLastCommand() {
commands.get(commands.size() - 1).undoCmd();
}
}
看到它引用了命令的列表。
命令抽象 Command.java
public interface Command {
void doCmd();
void undoCmd();
}
具體命令 ShapeDrawing.java
public class ShapeDrawing implements Command {
private ShapeDrawer drawer;
private String arg;
public ShapeDrawing(ShapeDrawer drawer, String arg) {
this.drawer = drawer;
this.arg = arg;
}
public void doCmd() {
drawer.drawShape(arg);
}
public void undoCmd() {
drawer.undrawShape();
}
}
具體命令 ColorFilling.java
public class ColorFilling implements Command {
private ColorFiller filler;
private String arg;
public ColorFilling(ColorFiller filler, String arg) {
this.filler = filler;
this.arg = arg;
}
public void doCmd() {
filler.fillColor(arg);
}
public void undoCmd() {
filler.unfillColor();
}
}
兩個(gè)具體命令都引用了相應(yīng)的命令執(zhí)行者。
執(zhí)行命令者 ShapeDrawer.java
public class ShapeDrawer {
public void drawShape(String shape) {
System.out.println("畫了一個(gè)" + shape);
}
public void undrawShape() {
System.out.println("撤銷剛才畫的形狀");
}
}
執(zhí)行命令者 ColorFiller.java
public class ColorFiller {
public void fillColor(String color) {
System.out.println("填充" + color);
}
public void unfillColor() {
System.out.println("撤銷填充的顏色");
}
}
測(cè)試一下,發(fā)出三個(gè)命令,并撤銷最后一個(gè):
Client.java
public class Client {
public static void main(String[] args) {
DrawingApp app = new DrawingApp();
ShapeDrawer shapeDrawer = new ShapeDrawer();
ColorFiller colorFiller = new ColorFiller();
Command drawCircle = new ShapeDrawing(shapeDrawer, "圓形");
Command fillRed = new ColorFilling(colorFiller, "紅色");
Command drawRectancle = new ShapeDrawing(shapeDrawer, "矩形");
app.takeCommand(drawCircle);
app.takeCommand(fillRed);
app.takeCommand(drawRectancle);
app.CommandsDone();
app.undoLastCommand();
}
}
輸出如下:
畫了一個(gè)圓形
填充紅色
畫了一個(gè)矩形
撤銷剛才畫的形狀
總結(jié)
通過以上例子,我們可以總結(jié)出命令模式的特點(diǎn):
- 將命令封裝為對(duì)象作為傳遞媒介;
- 命令發(fā)出者(invoker)和命令執(zhí)行者(receiver)是解耦的,通過命令對(duì)象“牽線”,invoker維護(hù)有命令(或命令集合)的引用,命令中有receiver的引用。
命令模式的應(yīng)用場(chǎng)景(轉(zhuǎn)載):
- Multi-level undo(多級(jí)undo操作)。如果系統(tǒng)需要實(shí)現(xiàn)多級(jí)回退操作,這時(shí)如果所有用戶的操作都以command對(duì)象的形式實(shí)現(xiàn),系統(tǒng)可以簡(jiǎn)單地用stack來保存最近執(zhí)行的命令,如果用戶需要執(zhí)行undo操作,系統(tǒng)只需簡(jiǎn)單地popup一個(gè)最近的command對(duì)象然后執(zhí)行它的undo()方法既可。
- Transactional behavior(原子事務(wù)行為)。借助command模式,可以簡(jiǎn)單地實(shí)現(xiàn)一個(gè)具有原子事務(wù)的行為。當(dāng)一個(gè)事務(wù)失敗時(shí),往往需要回退到執(zhí)行前的狀態(tài),可以借助command對(duì)象保存這種狀態(tài),簡(jiǎn)單地處理回退操作。
- Progress bars(狀態(tài)條)。假如系統(tǒng)需要按順序執(zhí)行一系列的命令操作,如果每個(gè)command對(duì)象都提供一個(gè)getEstimatedDuration()方法,那么系統(tǒng)可以簡(jiǎn)單地評(píng)估執(zhí)行狀態(tài)并顯示出合適的狀態(tài)條。
- Wizards(導(dǎo)航)。通常一個(gè)使用多個(gè)wizard頁(yè)面來共同完成一個(gè)簡(jiǎn)單動(dòng)作。一個(gè)自然的方法是使用一個(gè)command對(duì)象來封裝wizard過程,該command對(duì)象在第一個(gè)wizard頁(yè)面顯示時(shí)被創(chuàng)建,每個(gè)wizard頁(yè)面接收用戶輸入并設(shè)置到該command對(duì)象中,當(dāng)最后一個(gè)wizard頁(yè)面用戶按下“Finish”按鈕時(shí),可以簡(jiǎn)單地觸發(fā)一個(gè)事件調(diào)用execute()方法執(zhí)行整個(gè)動(dòng)作。通過這種方法,command類不包含任何跟用戶界面有關(guān)的代碼,可以分離用戶界面與具體的處理邏輯。
- GUI buttons and menu items(GUI按鈕與菜單條等等)。Swing系統(tǒng)里,用戶可以通過工具條按鈕,菜單按鈕執(zhí)行命令,可以用command對(duì)象來封裝命令的執(zhí)行。
- Thread pools(線程池)。通常一個(gè)典型的線程池實(shí)現(xiàn)類可能有一個(gè)名為addTask()的public方法,用來添加一項(xiàng)工作任務(wù)到任務(wù)隊(duì)列中。該任務(wù)隊(duì)列中的所有任務(wù)可以用command對(duì)象來封裝,通常這些command對(duì)象會(huì)實(shí)現(xiàn)一個(gè)通用的接口比如java.lang.Runnable。
- Macro recording(宏紀(jì)錄)。可以用command對(duì)象來封裝用戶的一個(gè)操作,這樣系統(tǒng)可以簡(jiǎn)單通過隊(duì)列保存一系列的command對(duì)象的狀態(tài)就可以記錄用戶的連續(xù)操作。這樣通過執(zhí)行隊(duì)列中的command對(duì)象,就可以完成"Play back"操作了。
- Networking。通過網(wǎng)絡(luò)發(fā)送command命令到其他機(jī)器上運(yùn)行。
- Parallel Processing(并發(fā)處理)。當(dāng)一個(gè)調(diào)用共享某個(gè)資源并被多個(gè)線程并發(fā)處理時(shí)。