联系方式
Java服务器开发群:66728073
游戏开发者高级群:398808948
Unity3d游戏开发:286114103
龙井,铁观音,茶叶
游戏系统设计开发之命令模式(一)
2016-10-07 16:18浏览数:859 

一,命令模式的定义

将来自客户端的请求传入一个对象,从而使你可用不同的请求对客户进行参数化。用于“行为请求者”与“行为实现者”解耦,可实现二者之间的松耦合,以便适应变化。分离变化与不变的因素。解决了这种耦合的好处我认为主要有两点:1.更方便的对命令进行扩展(注意:这不是主要的优势,后面会提到)2.对多个命令的统一控制(这种控制包括但不限于:队列、撤销/恢复、记录日志等等)在面向对象的程序设计中,一个对象调用另一个对象,一般情况下的调用过程是:创建目标对象实例;设置调用参数;调用目标对象的方法。但在有些情况下有必要使用一个专门的类对这种调用过程加以封装,我们把这种专门的类称作command类。

Command模式可应用于

a)整个调用过程比较繁杂,或者存在多处这种调用。这时,使用Command类对该调用加以封装,便于功能的再利用。

b)调用前后需要对调用参数进行某些处理。

c)调用前后需要进行某些额外处理,比如参数合法性验证,日志,缓存,记录历史操作等。

Command模式有如下效果:

a)将调用操作的对象和知道如何实现该操作的对象解耦。

bCommand是头等对象。他们可以像其他对象一样被操作和扩展。

c)你可将多个命令装配成一个符合命令。

d)增加新的Command很容易,因为这无需改变现有的类。

经典的命令模式包括4个角色:

Command:定义命令的统一接口

ConcreteCommandCommand接口的实现者,用来执行具体的命令,某些情况下可以直接用来充当Receiver

Receiver:命令的实际执行者

Invoker:命令的请求者,是命令模式中最重要的角色。这个角色用来对各个命令进行控制。

经典的命令模式包括4个角色:

ICommand接口:定义命令的统一接口

ConcreteCommand具体的命令实现类ICommand接口的实现者,用来执行具体的命令,某些情况下可以直接用来充当Receiver

Receiver:命令的实际执行者

Invoker:命令的请求者,是命令模式中最重要的角色。这个角色用来对各个命令进行控制。

下面对上面四个角色的经典实现用代码来进行说明,这也是大部分文章对命令模式的运用方式。

   

   public interface ICommand

    {

        void Execute();

    }

 

   public class ConcreteCommandA : ICommand

   {

       private Receiver receiver = null;

       public ConcreteCommandA(Receiver receiver)

       {

           this.receiver = receiver;

       }

       public void Execute()

       {

           this.receiver.DoA();

       }

   }

 

   public class ConcreteCommandB : ICommand

   {

       private Receiver receiver = null;

       public ConcreteCommandB(Receiver receiver)

       {

           this.receiver = receiver;

       }

       public void Execute()

       {

           this.receiver.DoB();

       }

   }

 

   public class Receiver

   {

       public void DoA()

       {

           //DoSomething

       }

       public void DoB()

       {

           //DoSomething

       }

   }

   public class Invoker

   {

       private ICommand command = null;

       //设置命令

       public void SetCommand(ICommand command)

       {

           this.command = command;

       }

       //执行命令

       public void RunCommand()

       {

           command.Execute();

       }

   }

   public class Client

   {

       public Client()

       {

           Receiver receiver = new Receiver();

           Invoker invoker = new Invoker();

           invoker.SetCommand(new ConcreteCommandA(receiver));

           invoker.RunCommand();

           invoker.SetCommand(new ConcreteCommandB(receiver));

           invoker.RunCommand();

       }

}


不知道大家看过上面的代码之后是什么感觉,反正我看过上面的代码之后第一反应确实是越看越糊涂了,主要觉得有几点疑问:

1. 执行命令可以,但是为什么要用命令封装起来,这不是有点脱裤子放屁的感觉么?我完全可以这样写:

public class Client

   {

       public Client()

       {

           Receiver receiver = new Receiver();

           receiver.DoA();

           receiver.DoB();

       }

 }

这样不是更加简单明了?两个类搞定。

2. 通过继承ICommand之后,增加命令怎么增加?比如增加一个命令,要改动3个地方:增加一个ICommand实现,修改Receiver类,修改Client。这好像没有对修改关闭啊?
天啦!这简直是个噩梦,完全没感觉到对修改关闭啊。难道是我对命令模式完全理解错误了吗?

3. ConcreteCommandAConcreteCommandBReceiver类完全耦合了啊,要是有ConcreteCommandAConcreteCommandB要执行的命令在不同的Receiver中怎么办?

看到这里,像我这种对设计模式一知半解的小伙伴估计完全懵了,在这话情况下对自己的智商产生了严重的怀疑,或者设计模式错了??但实际情况真的是这样吗?NONONONO,可以想到绝对不是,这可是奉为经典的设计模式啊,好吧,那我们来看看到底错在哪里:

1. 确实可以两个类来搞定。但我们要牢记命令模式的初衷:对命令请求者(Invoker)和命令实现者(Receiver)的解耦,方便对命令进行各种控制。打个比方:现在我们要对ConcreteCommandAConcreteCommandB以及其他一系列命令进行日志记录,并且两个命令之间的操作间隔不能大于1秒。

这种情况下要直接用两个类就会有大量的业务逻辑要在客户端进行处理,当命令增加,对每个命令的控制增加时,就会在Client里面产生大量的变化点,这样耦合就出来了,但是采用命令模式之后,对着一系列的命令我们都可以进行控制,这就是对变化点的封装,实际Invoker代码如下:

public class Invoker

   {

       private ICommand lastCommand = null;

       private DateTime lastDateTime = DateTime.Now;

       public void RunCommand(ICommand command)

       {

           //记录操作日志

           Console.WriteLine(command.GetType().Name);

           //大于1秒,执行命令

           if (lastCommand == null || (DateTime.Now - this.lastDateTime).TotalSeconds > 1)

           {

               lastCommand = command;

               lastDateTime = DateTime.Now;

               command.Execute();

           }

           //小于1秒时不执行,并进行相应处理

           Console.WriteLine("操作间隔过短!");

       }

   }


2. 增加命令:采用命令模式的时候,我感觉最大的耦合点变化到了ReceiverConcreteCommand之间,当然我们可以对Receiver进行抽象,采用接口或者抽象类来封装这个变化,但实际情况中我们会遇到多个命令来至于不同的Receiver,比如A,B两个命令来至于ReceiverAB,C命令来至于ReceiverC,这种情况下我们怎么应对命令的新增?对这种情况我的理解是命令模式并不能也不需要解决这个问题,因为命令模式的操作单元已经细化到了每一个具体的功能上面,当增加一个具体功能的时候是没有很好的办法对功能实现类进行修改关闭的(当然你可以把每个功能方法放到一个类中,但确实没必要,这个粒度已经很小了),实际上也没有必要的。

打个比方:一个界面有增加删除功能,在一个类Receiver里面实现了,现在要新增一个修改功能,有必要新增一个Reeciver类吗?我自己的答案是没有必要。

适用场景:

1. 命令的发送者和命令执行者有不同的生命周期。命令发送了并不是立即执行。

2. 命令需要进行各种管理逻辑。

3. 需要支持撤消\重做操作(这种状况的代码大家可以上网搜索下,有很多,这里不进行详细解读)。

结论:

通过对上面的分析我们可以知道如下几点:

1. 命令模式是通过命令发送者和命令执行者的解耦来完成对命令的具体控制的。

2. 命令模式是对功能方法的抽象,并不是对对象的抽象。

3. 命令模式是将功能提升到对象来操作,以便对多个功能进行一系列的处理以及封装。

下一节将用一个游戏开发的例子来说明命令模式的具体使用。


转载请注明来自游戏技术网:http://www.youxijishu.com