意圖<?XML:NAMESPACE PREFIX = O />
將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊或記錄請求日誌,以及支援可撤消的操作。
場景
我們知道,網路遊戲中的客戶端需要不斷把當前人物的資訊傳送到遊戲服務端進行處理(計算合法性、儲存狀態到資料庫等)。假設有這樣一種需求,在服務端收到客戶端的請求之後需要判斷兩次請求間隔是不是過短,如果過短的話就考慮可能是遊戲外掛,不但不執行當前請求還要把前一次請求進行回滾。暫且把問題簡單化一點不考慮客戶端和服務端之間的通訊來進行程式設計的話,你可能會建立一個Man型別,其中提供了一些人物移動的方法,執行這些方法後,服務端記憶體中的人物會進行一些座標的修改。客戶端定時呼叫Man型別中的這些方法即可。那麼如何實現防外掛的需求呢?你可能會想到在Man方法中儲存一個列表,每次客戶端呼叫方法的時候把方法名和方法呼叫的時間儲存進去,然後在每個方法執行之前就進行判斷。這樣做有幾個問題:
l Man型別應該只是負責執行這些操作的,是否應該執行操作的判斷放在Man型別中是否合適?
l 如果方法的呼叫還有引數的話,是不是需要把方法名、方法的引數以及方法呼叫時間都儲存到列表中呢?
l 如果需要根據不同的情況回滾一組行為,比如把Man型別的方法分為人物移動和裝備損耗,如果客戶端傳送命令的頻率過快希望回滾所有人物移動的行為,如果客戶端傳送命令的頻率過慢希望回滾所有裝備損耗的行為。遇到這樣的需求怎麼實現呢?
由此引入命令模式,命令模式的主要思想就是把方法提升到型別的層次,這樣對方法的執行有更多的控制力,這個控制力表現在對時間的控制力、對撤銷的控制力以及對組合行為的控制力。
示例程式碼
using System; using System.Collections.Generic; using System.Text;
namespace CommandExample { class Program { static void Main(string[] args) { Man man = new Man(); Server server = new Server(); server.Execute(new MoveForward(man, 10)); System.Threading.Thread.Sleep(50); server.Execute(new MoveRight(man, 10)); server.Execute(new MoveBackward(man, 10)); server.Execute(new MoveLeft(man, 10)); } }
class Man { private int x = 0; private int y = 0;
public void MoveLeft(int i) { x -= i; }
public void MoveRight(int i) { x += i; }
public void MoveForward(int i) { y += i; }
public void MoveBackward(int i) { y -= i; }
public void GetLocation() { Console.WriteLine(string.Format("({0},{1})", x, y)); } }
abstract class GameCommand { private DateTime time;
public DateTime Time { get { return time; } set { time = value; } }
protected Man man;
public Man Man { get { return man; } set { man = value; } }
public GameCommand(Man man) { this.time = DateTime.Now; this.man = man; }
public abstract void Execute();
public abstract void UnExecute(); }
class MoveLeft : GameCommand { int step;
public MoveLeft(Man man, int i) : base(man) { this.step = i; }
public override void Execute() { man.MoveLeft(step); }
public override void UnExecute() { man.MoveRight(step); } }
class MoveRight : GameCommand { int step;
public MoveRight(Man man, int i) : base(man) { this.step = i; }
public override void Execute() { man.MoveRight(step); }
public override void UnExecute() { man.MoveLeft(step); } }
class MoveForward : GameCommand { int step;
public MoveForward(Man man, int i) : base(man) { this.step = i; }
public override void Execute() { man.MoveForward(step); }
public override void UnExecute() { man.MoveBackward(step); } }
class MoveBackward : GameCommand { int step;
public MoveBackward(Man man, int i) : base(man) { this.step = i; }
public override void Execute() { man.MoveBackward(step); }
public override void UnExecute() { man.MoveForward(step); } }
class Server { GameCommand lastCommand;
public void Execute(GameCommand cmd) { Console.WriteLine(cmd.GetType().Name); if (lastCommand !=null && (TimeSpan)(cmd.Time - lastCommand.Time) < new TimeSpan(0, 0, 0, 0, 20)) { Console.WriteLine("Invalid command"); lastCommand.UnExecute(); lastCommand = null; } else { cmd.Execute(); lastCommand = cmd; } cmd.Man.GetLocation(); } } } |
程式碼執行結果如下圖:
程式碼說明
l 在程式碼例項中,我們只考慮了防止請求過頻的控制,並且也沒有考慮客戶端和服務端通訊的行為,在實際操作中並不會這麼做。
l Man類是接受者角色,它負責請求的具體實施。
l GameCommand類是抽象命令角色,它定義了統一的命令執行介面。
l MoveXXX型別是具體命令角色,它們負責執行接受者物件中的具體方法。從這裡可以看出,有了命令角色,傳送者無需知道接受者的任何介面。
l Server類是呼叫者角色,相當於一個命令的大管家,在合適的時候去呼叫命令介面。
何時採用
有如下的需求可以考慮命令模式:
l 命令的發起人和命令的接收人有不同的生命週期。比如,下遺囑的這種行為就是命令模式,一般來說遺囑執行的時候命令的發起人已經死亡,命令是否得到有效的執行需要靠律師去做的。
l 希望能讓命令具有物件的性質。比如,希望命令能儲存以實現撤銷;希望命令能儲存以實現佇列化操作。撤銷的行為在GUI中非常常見,佇列化命令在網路操作中也非常常見。
l 把命令提升到類的層次後我們對類行為的擴充套件就會靈活很多,別的不說,我們可以把一些建立型模式和結構型模式與命令模式結合使用。
實現要點
l 從活動序列上來說通常是這樣的一個過程:客戶端指定一個命令的接受者;客戶端建立一個具體的命令物件,並且告知接受者;客戶端通過呼叫者物件來執行具體命令;呼叫者物件在合適的時候發出命令的執行指令;具體命令物件呼叫命令接受者的方法來落實命令的執行。
l 命令模式從結構上說變化非常多,要點就是一個抽象命令介面。抽象命令介面包含兩個含義,一是把方法提升到類的層次,二是使用統一的介面來執行命令。
l 有了前面說的這個前提,我們才可以在呼叫者角色中做很多事情。比如,延遲命令的執行、為執行的命令記錄日誌、撤銷執行的命令等等。
l 在應用的過程中可以省略一些不重要的角色。比如,如果只有一個執行者或者執行的邏輯非常簡單的話,可以把執行的邏輯合併到具體命令角色中;如果我們並不需要使用呼叫者來做額外的功能,僅僅是希望通過命令模式來解除客戶端和接受者之間耦合的話可以省略呼叫者角色。
l 如果需要實現類似於巨集命令的命令組可以使用組合模式來封裝具體命令。
l 如果需要實現undo操作,那麼命令接受者通常也需要公開undo的介面。在應用中,undo操作往往不是呼叫一下undo方法這麼簡單,因為一個操作執行後所改變的環境往往是複雜的。
注意事項
l 不要被命令模式複雜的結構所迷惑,如果你不能理解的話請思考這句話“把方法提升到類的層次的好處也就是命令模式的好處”。
l 和把狀態或演算法提到類的層次的狀態模式或策略模式相比,命令模式可能會產生更多的類或物件。