設計模式(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#的工程化實現及擴充套件》