引子:烤羊肉串引來的思考#

小區門口的新疆烤肉攤生意火爆,但場面混亂:

  • 老闆一個人忙不過來,分不清誰是誰
  • 大家七嘴八舌:「這串不太熟」「我先給的錢」「我是不辣的」
  • 老闆容易收錢錯誤、串數錯誤、烤肉品質不一

顧客(行為請求者)與烤肉者(行為實現者)緊耦合——容易出錯、容易混亂、容易挑剔。

對比烤肉店:

  • 顧客點菜 → 服務員記錄訂單 → 廚師按序製作
  • 顧客不必盯著廚房;服務員寫單可改、可撤、可記錄、可結算

烤肉店比路邊攤的差別,正好對應一個重要的設計模式:命令模式

緊耦合設計(路邊攤版本)#

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 --> Receiver
sequenceDiagram
    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]

何時使用?#

不是只要有請求就用命令模式。

敏捷開發原則:不要為程式碼添加基於猜測的、實際不需要的功能

如果不清楚系統是否需要命令模式,不要急著實作——當真正需要撤銷/恢復等功能時,再透過重構實現也不困難。