1. 程式人生 > 實用技巧 >設計模式(16) 命令模式

設計模式(16) 命令模式

  • 命令模式
  • 適用場景
  • Redo & Undo
  • 命令模式的優缺點

命令模式

命令模式是對一類物件公共操作的抽象,它們具有相同的方法簽名,所以具有類似操作,可以被抽象出來,成為一個抽象的“命令”物件。請求以命令的形式包裹在物件中,並傳給呼叫物件。呼叫者尋找可以處理該命令的合適的物件,並把該命令傳給相應的物件,該物件執行命令。這樣實際操作的呼叫者就不是和一組物件打交道,它只需要依賴於這個“命令”物件的方法簽名,並根據這個操作簽名呼叫相關的方法。

GOF對命令模式描述為:
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests,and support undoable operations...


— Design Patterns : Elements of Reusable Object-Oriented Software

UML類圖:

程式碼示例:

public interface ICommand
{
    void Execute();
    Receiver Receiver { set; }
}

public class Receiver
{
    public string Name { get; private set; }
    public string Address { get; private set; }

    public void SetName()
    {
        this.Name = "Name";
    }

    public void SetAddress()
    {
        this.Name = "Address";
    }
}

public abstract class CommandBase : ICommand
{
    public Receiver Receiver { set; get; }

    public abstract void Execute();
}

public class SetAddressCommand : CommandBase
{
    public override void Execute()
    {
        base.Receiver.SetName();
    }
}

public class SetNameCommand : CommandBase
{
    public override void Execute()
    {
        base.Receiver.SetAddress();
    }
}

public class Invoker
{
    private IList<ICommand> commands = new List<ICommand>();
    public void AddCommand(ICommand command)
    {
        commands.Add(command);
    }

    public void Run()
    {
        foreach (ICommand command in commands)
        {
            command.Execute();
        }
    }
}

Client程式碼:

Receiver receiver = new Receiver();
ICommand command1 = new SetNameCommand();
ICommand command2 = new SetAddressCommand();
command1.Receiver = receiver;
command2.Receiver = receiver;
Invoker invoker = new Invoker();
invoker.AddCommand(command1);
invoker.AddCommand(command2);
invoker.Run();

適用場景

  • 呼叫者同時與多個執行物件互動,而且每個操作可以抽象為近似的形式。
  • 我們需要控制呼叫本身的生命期,而不是呼叫者直截了當地進行一個呼叫,有可能根據需要合併、分配、疏導相關的呼叫。
  • 一系列類似的呼叫可能需要輔以Redo()或Undo()之類的特性。
  • 類似以往函式指標,需要在執行一個呼叫的同時告訴它需要回調那些操作。
  • 方法本身太過複雜,從整個專案重用的角度考慮,需要把方法的實現抽象為一組可以協作的物件。

Redo & Undo

再來看看如何用命令模式實現Redo和Undo,要實現Redo和Undo就需要儲存執行過的命令,並通過安排這些命令的執行順序來達到目地。
以SQL的執行為例,下面的程式碼定義了SQLExecute作為Receiver,CommandManager作為Invoker,InsertIntoCommand作為ConcreteCommand:

public interface ICommand
{
    public void Execute();
    public void Undo();
}

public class SQLExcute
{
    public void InsertInto(string id)
    {
        Console.WriteLine("插入一條資料,id:" + id);
    }

    public void Delete(string id)
    {
        Console.WriteLine("刪除一條資料,id:" + id);
    }
}

public class InsertIntoCommand : ICommand
{
    private SQLExcute sqlExcute;
    private string id;
    public InsertIntoCommand(SQLExcute sqlExcute, string id)
    {
        this.sqlExcute = sqlExcute;
        this.id = id;
    }

    public void Execute()
    {
        sqlExcute.InsertInto(id);
    }

    public void Undo()
    {
        sqlExcute.Delete(id);
    }
}

public class CommandManager
{
    private Stack<ICommand> undoStacks = new Stack<ICommand>();
    private Stack<ICommand> redoStacks = new Stack<ICommand>();

    public void Execute(ICommand command)
    {
        command.Execute();
        undoStacks.Push(command);
        if (redoStacks.Count > 0)
        {
            redoStacks.Clear();
        }
    }

    public void Undo()
    {
        if (undoStacks.Count > 0)
        {
            ICommand pop = undoStacks.Pop();
            pop.Undo();
            redoStacks.Push(pop);
        }
    }

    public void Redo()
    {
        if (redoStacks.Count > 0)
        {
            ICommand pop = redoStacks.Pop();
            pop.Execute();
        }
    }
}

Client程式碼:

CommandManager manager = new CommandManager();
SQLExcute excute = new SQLExcute();
InsertIntoCommand command1 = new InsertIntoCommand(excute, "1");
InsertIntoCommand command2 = new InsertIntoCommand(excute, "2");
manager.Execute(command1);
manager.Execute(command2);

Console.WriteLine("undo------------");
manager.Undo(); 
manager.Undo();
Console.WriteLine("redo------------");
manager.Redo();
manager.Redo();

執行結果:

插入一條資料,id:1
插入一條資料,id:2
undo------------
刪除一條資料,id:2
刪除一條資料,id:1
redo------------
插入一條資料,id:1
插入一條資料,id:2

命令模式的優缺點

可以看到使用命令模式,呼叫者並不需要直接與實際的執行者打交道,實現了兩者的解耦,此外基於命令的機制,可以方便地做一些類似Undo, Redo的擴充套件,具體的優點有:
優點:

  • 命令模式將請求一個操作的物件與具體執行一個操作的物件分割開,符合開閉原則和迪米特法則
  • 能較容易的設計一個命令佇列
  • 在需要的情況下,可以容易的將命令計入日誌
  • 允許接收請求的一方決定是否接受請求
  • 可以容易的實現對請求的Undo,Redo
  • 由於加進新的具體命令類不影響其他的類,因此便於擴充套件

缺點:
命令模式也有其固有的缺點:在命令擴充至較多的數量時,便需要建立對應數量的ConcreteCommand,命令類過多,系統的維護會比較複雜。

參考書籍:
王翔著 《設計模式——基於C#的工程化實現及擴充套件》