一、前言 什麼是命令模式? 在軟體系統中,“行為請求者”與“行為實現者”通常呈現一種“緊耦合”。但在某些場合,比如要對行為進行“記錄、撤銷/重做、事務”等處理,這種無法抵禦變化的緊耦合是不合適的。在這種情況下,如何將“行為請求者”與“行為實現者”解耦?將一組行為抽象為對象,實現二者之間的松耦合,這就 ...
在軟體系統中,“行為請求者”與“行為實現者”通常呈現一種“緊耦合”。但在某些場合,比如要對行為進行“記錄、撤銷/重做、事務”等處理,這種無法抵禦變化的緊耦合是不合適的。在這種情況下,如何將“行為請求者”與“行為實現者”解耦?將一組行為抽象為對象,實現二者之間的松耦合,這就是命令模式(Command Pattern)——摘自百度百科
它解決了什麼問題?
解除行為請求者和行為實現者的高耦合。
比如,有一個業務,最開始的時候是:A->B
然後,有一天產品說,B這個資源比較敏感,每次調用B之前要先記錄一下日誌,假如說記錄日誌的業務邏輯是C,然後業務邏輯就會變成下麵這個樣子:
A -> C, A-> B
再然後,產品又說,B這個資源啊,有點不穩定,如果調用失敗的時候,能不能重試3次?然後業務邏輯就變成這個樣子:
A->C, retry 3 :A->B
後來,產品覺得B資源...,不這次不是B資源了,出現了一個新需要,處理的邏輯和B有點像,能不能$%^&?
結果,那塊代碼就變得無法維護了。。。如果有一天,需要去重構它,或者碰到一個類似的需求的時候,如何不讓自己難受,不給別人挖坑?
命令模式買不了吃虧,買不了上當!
二、以餐廳點餐,瞭解命令模式
舉個例子:假如你去一個餐廳吃烤魚,從叫服務員點菜,到服務員通知師傅做菜,到最後服務員上菜,這個流程就可以描述成一個命令模式,類圖如下:
每個角色和命令模式的幾個核心要素對應關係如上圖所示。
Customer(Client)
/** * 顧客(Client) */ public class Customer { public static void main(String[] args) { Order order = new BakeFishOrder(new Chef("李")); Waiter waiter = new Waiter(); waiter.takeOrder(order); waiter.orderUp(); } }
顧客需要做3個事情:
2,把菜單交給服務員
3,服務員上菜
顧客不關心這個烤魚的製作流程,只希望,我點菜以後,能夠按時把菜上上來就OK了,在實際的業務中,甚至可以把 new Chef() 這個操作也放在其他地方實現
比如具體的Command裡面,或者Invoker裡面,讓用戶感知不到,這樣業務的邏輯會變得更加平滑,總的來說就是:只關註自己需要的東西
Waiter(Invoker)
/** * 服務員(Invoker) */ public class Waiter { private Order order; public void takeOrder(Order order){ System.out.println("服務員接到訂單!"); this.order = order; } public void orderUp(){ System.out.println("服務員呼叫後臺準備烹飪訂單中的菜品"); order.execute(); System.out.println("訂單菜品已經烹飪完成,服務員上菜!"); } }
服務員這裡需要做兩件事情:
1,接單
2,通知廚房做菜並上菜
對於服務員來說,他也不需要關心這道菜誰做的,怎麼做的,只需要給廚房說了以後,過段時間把做好的菜送到客戶的手上就OK了
Order(Command)
/** * 訂單(Command) */ public interface Order { void execute(); }
訂單是一個抽象的東西,具體幹活的是訂單介面實現類,引入訂單的作用是面向抽象編程,以後每加一種菜,只需要增加一個實現類就可以了
BakeFishOrder(ConncereteCommand)
/** * 烤魚訂單(具體的訂單對象) */ public class BakeFishOrder implements Order{ private Chef chef; public BakeFishOrder(Chef chef){ this.chef = chef; } public void execute() { chef.bakeFish(); } }
烤魚訂單這裡就是一個具體的訂單,在它的execute方法中就需要去調用真正的實現者的一個或一些方法來實現製作烤魚這樣的一個需求。
/** * 廚師(Receiver) */ public class Chef { private String name; public Chef(String name){ this.name = name; } public void bakeFish(){ System.out.println(name + "師傅在製作烤魚..."); } }
在這個例子中,廚師就是最後幹活的對應命令模式中的Receiver,繞這麼大一圈,也就是為了顧客(行為請求者)和 廚師(行為實現者)的解耦。
運行結果:
顧客和廚師解耦的好處是什麼?
1,在點餐的流程不變的情況下,可以隨意增加新菜(Order的實現類)
2,在點餐的流程不變的情況下,原菜品的製作方式變化,上層是無需做任何修改的
三、遙控器需求
HeadFirst上,需要實現的是一個遙控器,遙控器用來遙控房間內的各種家電,並且可以實現undo功能。
但是有幾個問題:
1,每個廠商的驅動標準是不一樣的,比如電燈,開關的方法是:on(), off(),弔扇的方法是:high(), medium(),low(),off()
2,遙控器直接調用具體的家電驅動的話,一旦新的廠商載入到遙控器上面的時候,就必須要改代碼,比如:電燈的開關方法變成了:dim(level),通過level來調節燈泡的明暗程度,調到0就是關,調到100就是開。
如何使用命令模式來實現呢?具體的流程如下:
/** * 描述:燈 */ public class Light { public static final int LIGHT_ON = 100;//打開燈時,燈的亮度 public static final int LIGHT_OFF = 0;//關閉燈時,燈的亮度 private String location;//燈的位置 private int level;//亮度 public Light(String location) { this.location = location; } //開燈 public void on() { level = LIGHT_ON; System.out.println(location + ", 燈已經打開!"); } //關燈 public void off() { level = LIGHT_OFF; System.out.println(location + ", 燈已經關閉!"); } /** * 調節燈的亮度 * @param level 亮度 */ public void dim(int level) { this.level = level; if (level == LIGHT_OFF) { off(); return; } if(level == LIGHT_ON){ on(); return; } throw new RuntimeException("無法調節到指定的亮度, level: " + level); } public int getLevel() { return level; } }View Code
/** * 描述:弔扇 */ public class CeilingFan { public static final int HIGH = 3;//3檔 public static final int MEDIUM = 2;//2檔 public static final int LOW = 1;//1檔 public static final int OFF = 0;//0檔 String location;//弔扇位置 private int speed;//當前速度 public CeilingFan(String location) { this.location = location; speed = OFF; } public void high() { speed = HIGH; System.out.println(location + ",弔扇檔位:" + HIGH); } public void medium() { speed = MEDIUM; System.out.println(location + ",弔扇檔位:" + MEDIUM); } public void low() { speed = LOW; System.out.println(location + ",弔扇檔位:" + LOW); } public void off() { speed = OFF; System.out.println(location + ",弔扇檔位:" + OFF); } public int getSpeed() { return speed; } }View Code
1,制定標準,每個家電都必須依照這個規範來進行開發,接入遙控器系統(Command介面)
/** * 描述:命令介面 */ public interface Command { /** * 描述:執行命令 */ void execute(); /** * 描述:撤銷命令 */ void undo(); }
2,根據需求先實現遙控器,再實現具體的命令(由上而下開發)
/** * 遙控器 */ public class RemoteControl { Command[] onCommands;//啟動命令 Command[] offCommands;//關閉命令 Command undoCommand;//撤銷命令 public RemoteControl() { //初始化遙控器,命令設置為預設實現類NoCommand,減少判斷邏輯,減少NLP概率 onCommands = new Command[7]; offCommands = new Command[7]; Command noCommand = new NoCommand(); for(int i=0;i<7;i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } /** * 設置命令 * @param slot 按鈕位置 * @param onCommand 啟動命令 * @param offCommand 關閉命令 */ public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } /** * 啟動按鈕按下時的操作 * @param slot 按鈕位置 */ public void onButtonWasPushed(int slot) { onCommands[slot].execute(); undoCommand = onCommands[slot]; } /** * 關閉按鈕按下時的操作 * @param slot 按鈕位置 */ public void offButtonWasPushed(int slot) { offCommands[slot].execute(); undoCommand = offCommands[slot]; } /** * 撤銷按鈕按下時的操作 */ public void undoButtonWasPushed() { undoCommand.undo(); } public String toString() { StringBuffer stringBuff = new StringBuffer(); stringBuff.append("\n------ 遙控器 -------\n"); for (int i = 0; i < onCommands.length; i++) { stringBuff.append("[位置 " + i + "] " + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "\n"); } stringBuff.append("[撤銷] " + undoCommand.getClass().getName() + "\n"); return stringBuff.toString(); } }
/** * 描述:命令的預設的空實現 */ public class NoCommand implements Command { public void execute() { } public void undo() { } }
3,實現電燈的開關
/** * 描述:開燈命令 */ public class LightOnCommand implements Command { private Light light; private int lastLightLevel; public LightOnCommand(Light light) { this.light = light; } public void execute() { lastLightLevel = light.getLevel(); light.on(); } public void undo() { light.dim(lastLightLevel); } }View Code
/** * 描述:關燈命令 */ public class LightOffCommand implements Command { private Light light; private int lastLightLevel; public LightOffCommand(Light light) { this.light = light; } public void execute() { lastLightLevel = light.getLevel(); light.off(); } public void undo() { light.dim(lastLightLevel); } }View Code
4,實現風扇的各個檔位
public class CeilingFanHighCommand implements Command { CeilingFan ceilingFan; int prevSpeed; public CeilingFanHighCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { prevSpeed = ceilingFan.getSpeed(); ceilingFan.high(); } public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } }View Code
public class CeilingFanMediumCommand implements Command { CeilingFan ceilingFan; int prevSpeed; public CeilingFanMediumCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { prevSpeed = ceilingFan.getSpeed(); ceilingFan.medium(); } public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } }View Code
public class CeilingFanLowCommand implements Command { CeilingFan ceilingFan; int prevSpeed; public CeilingFanLowCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { prevSpeed = ceilingFan.getSpeed(); ceilingFan.low(); } public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } }View Code
public class CeilingFanOffCommand implements Command { CeilingFan ceilingFan; int prevSpeed; public CeilingFanOffCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { prevSpeed = ceilingFan.getSpeed(); ceilingFan.off(); } public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } }View Code
當然這種並不是最好的方式,undo中的代碼被寫了幾次,這樣,哪裡有重覆,哪裡就有抽象(實際業務中,請根據具體業務選擇是否抽象,如果抽象了發現業務更難做了,就要考慮是不是抽象的方向有問題了)
public abstract class AbstractCeilingFanCommand implements Command{ private CeilingFan ceilingFan; private int prevSpeed; public AbstractCeilingFanCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { prevSpeed = ceilingFan.getSpeed(); doExecute(); } protected abstract void doExecute(); public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } protected CeilingFan getCeilingFan() { return ceilingFan; } }
重新實現各個檔位
public class CeilingFanHighCommand extends AbstractCeilingFanCommand{ public CeilingFanHighCommand(CeilingFan ceilingFan) { super(ceilingFan); } protected void doExecute() { getCeilingFan().high(); } }View Code
public class CeilingFanMediumCommand extends AbstractCeilingFanCommand{ public CeilingFanMediumCommand(CeilingFan ceilingFan) { super(ceilingFan); } protected void doExecute() { getCeilingFan().medium(); } }View Code
public class CeilingFanLowCommand extends AbstractCeilingFanCommand { public CeilingFanLowCommand(CeilingFan ceilingFan) { super(ceilingFan); } protected void doExecute() { getCeilingFan().low(); } }View Code
public class CeilingFanOffCommand extends AbstractCeilingFanCommand { public CeilingFanOffCommand(CeilingFan ceilingFan) { super(ceilingFan); } protected void doExecute() { getCeilingFan().off(); } }View Code
最後是測試類:
public class TestClient { private static RemoteControl REMOTE_CONTROL = new RemoteControl(); static{ //初始化燈也可以是抽象的,因為,Light的命令,並不關心是什麼燈 Light livingRoomLight = new Light("卧室"); //設置開燈,關燈命令 REMOTE_CONTROL.setCommand(0, new LightOnCommand(livingRoomLight), new LightOffCommand(livingRoomLight)); //初始化弔扇 CeilingFan ceilingFan = new CeilingFan("卧室"); //初始化弔扇每個檔位的命令 CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan); REMOTE_CONTROL.setCommand(1, new CeilingFanLowCommand(ceilingFan), ceilingFanOff); REMOTE_CONTROL.setCommand(2, new CeilingFanMediumCommand(ceilingFan), ceilingFanOff); REMOTE_CONTROL.setCommand(3, new CeilingFanHighCommand(ceilingFan), ceilingFanOff); } public static void main(String[] args) { System.out.println(REMOTE_CONTROL); REMOTE_CONTROL.onButtonWasPushed(0); REMOTE_CONTROL.offButtonWasPushed(0); System.out.print("撤銷命令執行:"); REMOTE_CONTROL.undoButtonWasPushed(); REMOTE_CONTROL.onButtonWasPushed(1); REMOTE_CONTROL.onButtonWasPushed(2); System.out.print("撤銷命令執行:"); REMOTE_CONTROL.undoButtonWasPushed(); REMOTE_CONTROL.onButtonWasPushed(3); System.out.print("撤銷命令執行:"); REMOTE_CONTROL.undoButtonWasPushed(); REMOTE_CONTROL.offButtonWasPushed(3); System.out.print("撤銷命令執行:"); REMOTE_CONTROL.undoButtonWasPushed(); } }
測試結果:
梳理一下,命令模式和這裡的對應關係:
Invoker:RemoteControl(遙控器)
Command:Command介面
ConcereteCommand:LightOnCommand,LightOffCommand,CeilingFanLowCommand....
Receiver:Light,CeilingFan
Client:TestClient
對TestClient和具體的驅動類(Light,CeilingFan)實現解耦,Client不直接調用驅動類,而是基於遙控器(Invoker)制定的標準,來調用具體的命令實現類。
這樣,在新增新的家電驅動的時候,只需要新增命令實現類和Client的初始化代碼即可,也可以做成配置文件,這樣就不用去改TestClient代碼了
四、命令模式優缺點
優點:
1,降低了請求者和執行者之間的耦合性
2,新命令的擴展性很強,需要新的命令,只需要加一個實現類即可,對現有代碼影響很小
3,命令之間還可以相互組合形成新的命令,比如在遙控器裡面要實現一個閃光效果,可以用LightOnCommand,LightOffCommand,結合起來做一個巨集命令
缺點:
命令的數量可能會很多,每個指令都必須封裝成一個類,如果指令過多,那麼開發的成本會很高
所以,只有在請求者和執行者真的需要解耦的時候才使用:
比如:A->B的調用中新增了很多操作,日誌,排隊,各種邏輯,需要引入Invoker
比如:有A->C邏輯和B相似,則可以引入Command介面來抽象這一流程
比如:B,C邏輯需要撤銷,或者重做的時候,也可以引入命令模式
總之,根據需要來抽象,沒有最好的,只有當下最合適的
或許,spring aop就很合適呢?