无废话C#设计模式之十八:Command
意图
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
场景
我们知道,网络游戏中的客户端需要不断把当前人物的信息发送到游戏服务端进行处理(计算合法性、保存状态到数据库等)。假设有这样一种需求,在服务端收到客户端的请求之后需要判断两次请求间隔是不是过短,如果过短的话就考虑可能是游戏外挂,不但不执行当前请求还要把前一次请求进行回滚。暂且把问题简单化一点不考虑客户端和服务端之间的通讯来进行程序设计的话,你可能会创建一个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 和把状态或算法提到类的层次的状态模式或策略模式相比,命令模式可能会产生更多的类或对象。