意圖
將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤消的操作。
場景
我們知道,網絡游戲中的客戶端需要不斷把當前人物的信息發送到游戲服務端進行處理(計算合法性、保存狀態到數據庫等)。假設有這樣一種需求,在服務端收到客戶端的請求之後需要判斷兩次請求間隔是不是過短,如果過短的話就考慮可能是游戲外掛,不但不執行當前請求還要把前一次請求進行回滾。暫且把問題簡單化一點不考慮客戶端和服務端之間的通訊來進行程序設計的話,你可能會創建一個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 和把狀態或算法提到類的層次的狀態模式或策略模式相比,命令模式可能會產生更多的類或對象。