引子:烤羊肉串引來的思考#
小區門口的新疆烤肉攤生意火爆,但場面混亂:
- 老闆一個人忙不過來,分不清誰是誰
- 大家七嘴八舌:「這串不太熟」「我先給的錢」「我是不辣的」
- 老闆容易收錢錯誤、串數錯誤、烤肉品質不一
顧客(行為請求者)與烤肉者(行為實現者)緊耦合——容易出錯、容易混亂、容易挑剔。
對比烤肉店:
- 顧客點菜 → 服務員記錄訂單 → 廚師按序製作
- 顧客不必盯著廚房;服務員寫單可改、可撤、可記錄、可結算
烤肉店比路邊攤的差別,正好對應一個重要的設計模式:命令模式。
緊耦合設計(路邊攤版本)#
public class Barbecuer
{
public void BakeMutton() => Console.WriteLine("烤羊肉串!");
public void BakeChickenWing() => Console.WriteLine("烤雞翅!");
}
// 客戶端
Barbecuer boy = new Barbecuer();
boy.BakeMutton();
boy.BakeMutton();
boy.BakeChickenWing();客戶端與「烤肉者」緊耦合,需求一多就僵化、隱患多。
鬆耦合設計(烤肉店版本)#
把每個烤肉動作封裝為命令物件,讓服務員只與抽象命令打交道:
public abstract class Command
{
protected Barbecuer receiver;
public Command(Barbecuer receiver) { this.receiver = receiver; }
public abstract void ExcuteCommand();
}
class BakeMuttonCommand : Command
{
public BakeMuttonCommand(Barbecuer receiver) : base(receiver) { }
public override void ExcuteCommand() => receiver.BakeMutton();
}
class BakeChickenWingCommand : Command
{
public BakeChickenWingCommand(Barbecuer receiver) : base(receiver) { }
public override void ExcuteCommand() => receiver.BakeChickenWing();
}服務員:
public class Waiter
{
private IList<Command> orders = new List<Command>();
public void SetOrder(Command command)
{
if (command.ToString() == "命令模式.BakeChickenWingCommand")
{
Console.WriteLine("服務員:雞翅沒有了,請點別的燒烤。");
return;
}
orders.Add(command);
Console.WriteLine($"增加訂單:{command} 時間:{DateTime.Now}");
}
public void CancelOrder(Command command)
{
orders.Remove(command);
Console.WriteLine($"取消訂單:{command} 時間:{DateTime.Now}");
}
public void Notify()
{
foreach (var cmd in orders) cmd.ExcuteCommand();
}
}客戶端:
Barbecuer boy = new Barbecuer();
Command bakeMutton1 = new BakeMuttonCommand(boy);
Command bakeMutton2 = new BakeMuttonCommand(boy);
Command bakeChickenWing = new BakeChickenWingCommand(boy);
Waiter girl = new Waiter();
girl.SetOrder(bakeMutton1);
girl.SetOrder(bakeMutton2);
girl.SetOrder(bakeChickenWing);
girl.Notify(); // 一次通知廚房製作服務員:
- 一次性通知(不是每點一個就通知一次)
- 可拒絕請求(雞翅沒了)
- 記錄日誌(以備收費/統計)
- 支援撤銷(取消已點訂單)
命令模式#
命令模式(Command Pattern):將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日誌,以及支援可撤銷的操作。[DP]
結構#
- Command(抽象命令):宣告執行操作的介面
- ConcreteCommand:將一個 Receiver 物件綁定於一個動作;呼叫 Receiver 相應操作以實現
Execute - Invoker(呼叫者):要求該命令執行這個請求
- Receiver(接收者):知道如何實施與執行一個請求相關的操作
classDiagram
class Client
class Invoker {
-Command command
+SetCommand(Command)
+ExecuteCommand()
}
class Command {
<<abstract>>
#Receiver receiver
+Execute()*
}
class ConcreteCommand {
+Execute()
}
class Receiver {
+Action()
}
Client ..> Receiver
Client ..> ConcreteCommand
Invoker o--> Command
Command <|-- ConcreteCommand
ConcreteCommand --> ReceiversequenceDiagram
participant 顧客
participant Waiter as 服務員
participant Cmd as BakeMuttonCommand
participant Boy as Barbecuer
顧客->>Waiter: SetOrder(羊肉串 x2)
顧客->>Waiter: SetOrder(雞翅)
Waiter-->>顧客: 雞翅沒有了,請點別的
顧客->>Waiter: Notify()
Waiter->>Cmd: ExcuteCommand()
Cmd->>Boy: BakeMutton()abstract class Command
{
protected Receiver receiver;
public Command(Receiver receiver) { this.receiver = receiver; }
public abstract void Execute();
}
class ConcreteCommand : Command
{
public ConcreteCommand(Receiver receiver) : base(receiver) { }
public override void Execute() => receiver.Action();
}
class Invoker
{
private Command command;
public void SetCommand(Command command) => this.command = command;
public void ExecuteCommand() => command.Execute();
}
class Receiver
{
public void Action() => Console.WriteLine("執行請求!");
}命令模式的優點#
- 可建立命令隊列
- 可記錄命令日誌
- 接收請求的一方可以否決請求
- 容易實現撤銷與重做
- 增加新的具體命令類不影響其他類
最關鍵的優點:把「請求一個操作的物件」與「知道怎麼執行一個操作的物件」分割開來。[DP]
何時使用?#
不是只要有請求就用命令模式。
敏捷開發原則:不要為程式碼添加基於猜測的、實際不需要的功能。
如果不清楚系統是否需要命令模式,不要急著實作——當真正需要撤銷/恢復等功能時,再透過重構實現也不困難。