JavaScript設計模式之命令模式【命令解耦】
在講解命令模式之前我們先來了解一個生活中的命令模式場景:
場景1:
醫院看病抓藥:
當你因為腎虛到醫院看醫生,醫生一番操作之後得出結論:要吃個療程【夏桑菊】、【小柴胡】(藥名純屬虛構,真的腎虛就找醫生),於是醫生開了個藥單【夏桑菊、小柴胡】,讓你拿著藥單到收費視窗;於是射射發抖的到收費視窗把藥單【夏桑菊、小柴胡】給到收費人員,收費人員不管給你看醫生的是誰,你是否得腎虛,他只要對著藥單得收費項進行收費,完成後在藥單上蓋個收費章,然後讓你拿著藥單到取藥視窗拿藥,於是你又跑到取藥視窗,將藥單【夏桑菊、小柴胡】給到工作人員,工作人員到各個藥櫃找到你要得藥給到你。
場景2:
後廚製作:
當你來到一家餐廳吃飯,你跟服務員說你要吃【韭菜、秋葵、生蠔】,於是服務員將寫在訂單紙【韭菜、秋葵、生蠔】,把它插到後廚視窗,大廚們拿到訂單紙【韭菜、秋葵、生蠔】,之後紅紅哈嘿把菜做好。
相信你很容易從上面的案例中z找到共同點,他們的工作模式。
對於場景一:
- 醫生:他並不知道給你收費的是誰,怎麼收費,也不知道誰給你拿藥,怎麼找到藥。他就知道給你一張藥單。
- 收費人:他並不知道是給你的藥單,他只需要按照藥單上面寫著的給你結賬,也不管你下一步要做什麼。
- 取藥人:他並不知道給你看到醫生是誰,給你結賬的是誰,只要藥單上蓋了章,就給你拿藥。
-你 :攜帶者藥單,到處找人辦事,你也不管收費人是怎麼給你收的,也不關心取藥人怎麼找到藥,你只需要將藥單交給他們。他們就知道怎麼做了
對於場景2:
- 你:你不用糾結誰給你做的菜,誰給你下的單,你就提出你的需求。
- 服務員:根據你的需求生成張訂單,然後給到後廚,不關心後廚怎麼做菜。
- 廚師:不關心吃的人是誰,服務員是誰,只需要拿到訂單,做出菜品。
總結:大家都只關注到【單子/命令】,而不關心其他人是怎麼做的。
引用《JavaScript設計模式及與開發實踐》中的原話:
命令模式最常見的應用場景:有時候需要向某些物件傳送請求,但是並不知道請求的接收者是誰,也不知道請求的操作是什麼,此時希望有一種鬆耦合的方式來設計程式,使得請求傳送者與請求接收者能夠消除彼此之間的耦合關係。
用場景2的例子套入原話:有時候【顧客】需要向【廚師】傳送請求【吃生蠔...】,但【顧客】實際上並不知道請求【吃生蠔】是哪個廚師做的(甚至生蠔在隔壁店的廚師幫忙都有可能),也不知道【廚師】接到請求【吃生蠔】之後他會怎麼做出這道菜,此時希望有一箇中間人【服務員】幫忙處理【顧客】跟【廚師】的關係,使得【顧客】跟【廚師】沒有產生耦合關係。而【服務員】是負責將【顧客】請求命令帶給【廚師】。
場景2案例如何程式碼實現:
首先我們要理清楚這種模式涉及到的角色:
由三種角色構成:
- 釋出者 invoker(發出命令,持有命令物件)
- 接收者 receiver (命令處理者,不知道誰發起請求)
- 命令 command(接收命令,分發給對應接收者處理,持有接收者)
首先我們來講解一個非常簡單的案例:
// 案例
// 接受者
class Receiver {
execute () {console.log('處理')};
}
// 命令
class Command {
constructor (receiver) {
this.receiver = receiver;
}
execute () {
this.receiver.execute();
}
}
// 釋出者
class Invoker {
constructor (command) {
this.command = command;
}
invoke () {
this.command.execute();
};
}
// 執行
let command = new Command(new Receiver());
let invoker = new Invoker(command);
invoker.invoke();
結合到案例2的場景當中:
// 廚師 擁有各項才藝
class Cook {
makeVegetables () {
console.log('make vegetable');
return 'vegetable';
}
makeFish () {
console.log('make fish');
return 'fish';
}
execute () {
this.makeFish();
this.makeVegetables();
}
};
// 命令物件
class Command {
constructor (cook) {
this.cook = cook;
}
execute () {this.cook.execute};
}
// 服務員 命令的釋出者
class Waiter {
constructor (command) {
this.command = command;
}
invokeCommand () {
this.command.execute();
};
}
其實在js中函式作為一等公民,能夠隨意傳遞的來講,有更加簡化程式碼的方式:
// 案例 ------------------------------ js 簡化 -----------------------------------------
class Receiver {
execute () {console.log('執行邏輯')};
}
class Invoker {
invoke (receiver) {
receiver.execute();
};
}
let invoker = new Invoker();
invoker.invoke(new Receiver());
我還是更加推薦上面的寫法,至少看起來更加清晰的結構,命令模式的使用更加易懂。
巨集命令
是一組命令的集合,通過執行巨集命令的方式,可以一次執行一組命令。
場景:如果你家裡買了很多小米的智慧家電,你希望一下班回來就自動都開啟燈,開啟空調,播放音樂...;這豈不美哉。
// 案例 ------------------------------ 巨集命令 -----------------------------------------
// 定義命令
class OpenLightCommand {
execute() {
console.log('open light');
}
}
class PlayMusicCommand {
execute() {
console.log('play light');
}
}
class OpenAirConditioningCommand {
execute() {
console.log('open air conditioning');
}
}
// 命令集合
class MacroCommand {
constructor() {
this.commandList = [];
}
add(command) {
this.commandList.push(command);
}
clear() {
this.commandList = [];
}
execute() {
for (let i = 0; i < this.commandList.length; i++) {
this.commandList[i].execute();
}
}
}
class ITMan {
constructor(command) {
this.commamd = command;
}
whenGoHome() {
this.commamd.execute();
}
}
let macroCommand = new MacroCommand();
macroCommand.add(new OpenLightCommand());
macroCommand.add(new PlayMusicCommand());
macroCommand.add(new OpenAirConditioningCommand());
let itMan = new ITMan(macroCommand);
itMan.whenGoHome();
總結:這種模式的寫法跟策略模式有很大的相似性,但目的性卻完全不同。命令模式更多的是為了解決請求方與實現方的解耦。