command模式的讨论

学习了Martin Fowler的重构以后,我就发现有必要学习一下设计模式。市面上有很多设计模式的书籍,我偏偏选择了GoF写的那本《设计模式》。这本书上的内容是难以想象的厚实,几乎涵盖了设计模式使用的所有细节和变化。因为我是新手,读这本书有一种惊心动魄的感觉,每一句话都随时有可能颠覆我的理解和认识。仅仅是从案例代码上学习到的知识,比很多经典教材来的都要多!
所以我打算写一篇关于设计模式的文章,来阐发或加深自己的理解,以比较难以掌握和理解的command模式为例。这不是一篇对command模式的教程,只是陈述我的个人认识,并追求将这一认识深化,以文档的形式明确下来。不保证我给出的案例完整呈现command模式的应用场合,也不保证command模式是这一问题的最佳解决方案。

1. 问题引入


1)概述:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。[GOF《设计模式》]command封装请求,使请求者与实现者松散耦合。

2)疑问:请求和命令是抽象的概念,如何封装成类?通过引入command对象这一中介,虽然可以使请求者和实现者解耦,但是不也增加了两者对command对象的依赖吗?将命令封装成类,为什么会产生解耦的效果?一些案例代码中给出的command对象实际上只是一个实现者的载体而已,而command对象的Execute操作只是简单的转发请求给实现者。客户将实现者作为构造函数的参数以创建command对象,再由command对象配置请求者(Invoker)对象,再调用请求者的相关操作,请求者再依靠command对象实现请求,command又将请求转发给实现者,这样做的意义何在?Invoker给人的感觉像是一个中介。
看似一个简单的概念,仔细想想自己还真不理解。只知道模式的作用和效果是不行的,应该知道为何产生这样的作用和效果。

2. 我的认识


1)何为请求/命令?
请求是一种消息传递,可以看做是一次方法的调用。比如在图形用户界面中,一次按钮的点击就触发了一个或多个请求。在这里,响应程序是Client,button是Invoker,button将请求转发给具体的实现者(请求接受者)执行功能。

2)command封装功能。
与其说command封装命令,我倒认为是封装功能。弱化实现者的概念,将更容易理解,因为command隐藏了功能的实现。一个command类定义了一个特定的功能,command自身具有执行命令的能力。
任何类都可能作为一个接受者(实现者) [GoF]。在构造command对象时传递实现者只是辅助其完成功能,任何参数都可以取代它的地位。command对象也未必需要实现者, 一个极端是command自己实现所有的功能,根本不需要额外的接受者对象[GoF]。换一种角度来看,就算是请求一个整数运算也是需要实现者的,command需要请求int对象的操作符方法进行运算,并返回一个int对象作为结果,这里的int对象就是实现者。所以实现者在command模式中是无足轻重的,属于command类的具体实现范畴,而设计模式讨论的是command类的作用(接口),而非实现。

                                               标准的command模式类说明图                                                                            理解的类说明图

3)参数化Invoker以实现解耦。
未引入command模式之前的场景是,Invoker由于依赖具体的功能或实现者而产生类层次,而不是由Invoker自身的特点产生类层次。在图形用户界面中,每一个菜单项支持不同的操作功能,每一项功能需要依赖不同的实现者完成。这就需要不断生成Invoker的子类来支持不断增添的功能,并且这样的功能固化在菜单项(Invoker)中,不能够复用在其他事宜的场合。比如有一个保存文件的菜单项,这个菜单项中直接引用了document对象以实现保存功能,而按ctrl+S也会保存文件,这时只能重新实现一遍。
引入command模式之后的场景是,Invoker只需依赖command,而command对象是动态绑定在类中的,Invoker不需要衍生子类也可以实现多态,这里的command对象就是一个state。Invoker和command分开创建,两者只在运行时绑定,可以具有不同的生命期。
以command的类层次取代Invoker的类层次。command可以在不同的抽象层次中复用,也可以使用组合的command支持命令脚本。
由于command对象隐藏了实现者,Invoker与Command抽象耦合,通过配置ConcreteCommand对象以参数化Invoker,这样Invoker与执行命令的具体操作无关,更与实现者无关,达到解耦的效果。

4)谁负责创建command对象?什么时候创建?
command封装了具体的功能,这是类层面的,功能固化在类中,不同的command类具有不同的功能。
●由CommandFactory负责创建command对象。CommandFactory是一个工厂类,具备创建command的条件。工厂根据客户提供的线索创建并返回一个满足其要求的ConcreteCommand对象,客户不需要清楚Command类层次结构。这里的command对象往往具有局部状态,是一次性的,不可共享。
●使用时创建command对象。一种情况是客户知道具体的Command对象,直接创建一个命令并执行它。比如在ADO.net中,执行一条查询命令,由客户直接创建SqlCommand对象并执行。这里的SqlCommand就封装了sql命令,并可以针对命令实现Cancel操作。
●事先创建Command对象。另一种情况是有时不知道要执行什么样的操作以及由谁来执行它,甚至不知道什么时候可能执行一些操作。这就需要在程序运行的开始创建这些Command对象并参数化Invoker,再由Invoker在合适的时候发起执行操作。比如在图形用户界面中,菜单项上的指令是持久存在的,用户只需触发相关事件就可以执行指令而无需创建指令,这就大大减轻了客户的负担。

5)独立的Command。
Command对象可以独立于Invoker而存在。一种情况是Command对象不具有局部状态,因此可以是Singleton。另一种情况是,Invoker可以将Command对象发送出去,而接受者保留这个对象而不会立即执行操作,这样即使发送者不复存在,原先的Command仍然可以在某时被调用,甚至可以记录在日志中。在多进程的运行环境中,可以利用这种方法智能化地处理请求。


3. 案例引入


在一个基于区块的小游戏中,游戏地图按行和列均匀划分为各个不同的区块,游戏人物位于这样的区块地图上。区块有不同的种类,区块上分布着一些事件,当游戏人物走到新的区块上时,就会触发对应区块上的事件。如获得道具事件会增加player相应道具,提升能力事件会提升player相应能力,控制移动事件将强制player移动到新的区块,陷阱事件会减少player的生命值并产生晃动游戏屏幕的效果,最后会向游戏消息栏发送一条消息。这些地图区块与事件以文件的形式保存,运行程序时读取这些游戏数据,在区块上设置准确的事件指令。
现在需要简单模拟这样的情况。


4. 方法选择


可以想到3种方法来模拟这种情况。

1)通过C#的事件机制来模拟游戏中的事件,这是一种Observer模式(发布-订阅)的运用。有一个委托接口

public delegate void CommandHandle(Queue<GameEventArgs> datas, Hero player);
有一些 实现委托接口的静态方法,这些静态方法从datas参数队列中弹出一条数据作为实际参数,可以实现特定命令的执行。区块(Invoker)创建时,订阅这些方法,以实现参数化功能的效果。这些静态方法没有状态所以可以被共享。

这种方法可以避免Command子类的生成,但是并不能记录调用状态,也不能实现撤销等需要多接口的操作。

2)通过Command模式封装这些细粒度的指令。每一个指令对应一个Command子类,可以通过反射或模板减少子类的数量。

定义一个MacroCommand类,它执行一个命令序列,由ConcreteCommand对象组合而成。Invoker只需依赖MacroCommand,这是一个安全组合模式的运用。

扩展Command的接口可以实现命令相关的其他的操作。

这是一种相对比较灵活的方法,我采用此方法来完成案例代码的编写。

3)定义一种命令序列的文法表示,并定义一个解释器解释这些文法。如:hp + 10 表示player的hp增加10;key - 1 and open 表示player的key道具减1,并调用区块上的Open方法(打开门)。还可以实现条件指令:if key > 0 then moveTo (4,5) 表示如果player存在key道具则移动到(4,5)位置。这样的文法指令存储在Invoker中,触发指令时交由解释器解释。这种方法更为灵活,但不可以存储局部状态,难以具备指令受时序变化的功能。


5. 参考代码

类静态图预览:

首先定义一个ICommand接口和IReceiver接口
    /// <summary>
    /// 定义执行操作的接口
    /// </summary>
    interface ICommand
    {
        /// <summary>
        /// 执行操作
        /// </summary>
        /// <param name="receiver">操作接收者</param>
        void Execute(IReceiver receiver);
    }

这里的IReceiver接口并不是命令的执行者,只是Command的参数而已,之所以称之为接受者是因为命令的执行会影响这个对象,即receiver接受Execute方法的影响。

IReceiver在执行事件时作为参数传递给Command而不是在构造时传递,这是因为receive可以是实现IReceiver接口的不同类的对象(如玩家和怪物),也可以是同一类的不同对象(如玩家1和玩家2),在Command创建时绑定不具备这样的灵活性。

    /// <summary>
    /// 定义接受者支持的接口集
    /// </summary>
    interface IReceiver
    {
        /// <summary>
        /// 获取或设置生命值
        /// </summary>
        int Hp { get; set; }

        /// <summary>
        /// 获取或设置持有的金币数量
        /// </summary>
        int Coins { get; set; }

        /// <summary>
        /// 指示人物移动到指定位置
        /// </summary>
        void MoveTo(int row, int col);

        // 其他的方法
    }

游戏角色实现IReceiver接口。游戏角色本身是一个抽象类,ICommand依赖IReceiver而不是GameRole是为了更易于编程。GameRole未来不仅仅继承IReceiver接口,还会实现其他的不同的功能,有必要只对ICommand公开一个窄接口。

    /// <summary>
    /// 游戏角色类
    /// </summary>
    abstract class GameRole : IReceiver
    {
        public int Hp { get; set; }

        public int Coins { get; set; }

        public void MoveTo(int row, int col)
        {
            //移动人物的操作
        }
    }

    class Hero : GameRole
    { 
        //勇士的局部状态和接口
    }

    class Monster : GameRole
    { 
        //怪物的局部状态和接口
    }

具体的Command:

    /// <summary>
    /// 增加血量命令
    /// </summary>
    class HpUpCommand : ICommand
    {
        private int _hpUpNumber;

        public HpUpCommand(int value) { this._hpUpNumber = value; }

        public void Execute(IReceiver receiver)
        {
            receiver.Hp += _hpUpNumber;
        }
    }

    /// <summary>
    /// 移动操作
    /// </summary>
    class MoveCommand : ICommand
    {
        private int _targetRow;
        private int _targetCol;

        public MoveCommand(int row, int col)
        {
            _targetCol = col;
            _targetRow = row;
        }

        public void Execute(IReceiver receiver)
        {
            receiver.MoveTo(_targetRow, _targetCol);
        }
    }

    /// <summary>
    /// 晃动屏幕命令
    /// </summary>
    class ShakeCommand : ICommand
    {
        public void Execute(IReceiver receiver)
        {
            //忽略receiver

            //晃动屏幕相关操作
        }
    }

MacroCommand继承自ICommand,增加了一些成员操作的接口,这是一个安全Composite模式的运用。

执行MacroCommand上的Execute方法将会依次调用各个节点的Execute方法。

    class Spirit { public void Show() { } }

    /// <summary>
    /// 组合命令
    /// </summary>
    class MacroCommand : ICommand
    {
        /// <summary>
        /// 命令脚本
        /// </summary>
        private List<ICommand> _commandScript = new List<ICommand>();

        /// <summary>
        /// 可视化精灵
        /// </summary>
        private Spirit _spirit;

        public MacroCommand(Spirit spirit) { this._spirit = spirit; }

        public void Execute(IReceiver receiver)
        {
            foreach (var item in _commandScript)
            {
                item.Execute(receiver);
            }
        }

        public void Append(ICommand cmd)
        {
            _commandScript.Add(cmd);
        }

        public void Remove(ICommand cmd)
        {
            _commandScript.Add(cmd);
        }

        /// <summary>
        /// 命令可视化
        /// </summary>
        public void Render()
        {
            _spirit.Show();
        }
    }

需要特别注意的是上面的Render方法,这是一个扩展接口,以支持命令可视化的功能。在区块地图中,存在事件的区块往往显示相应的图形。比如一个具有增加金币事件的区块会显示一个金币的图形来提示玩家,区块地图上形形色色的道具、背景都具有相应的事件,或者说,事件具有形状。事件对应一串命令序列,而命令序列可以用MacroCommand表示,那么MacroCommand有必要具有可视化显示的功能。

就像Command模式可以支持UnDo和ReDo操作一样,让command可视化也是可能的,只要增加一个Render接口即可。


Invoker(TileInvoker)依赖MacroCommand实现事件执行,这要求创建时将所有ICommand对象装载到MacroCommand对象中。

Invoker调用MacroCommand的Execute执行事件,渲染时调用MacroCommand的Render方法。

    /// <summary>
    /// 定义游戏区块 -- 调用者角色
    /// </summary>
    abstract class TileInvoker
    {
        /// <summary>
        /// 命令
        /// </summary>
        private MacroCommand _command;

        /// <summary>
        /// 获取区块命令集
        /// </summary>
        public MacroCommand Command
        {
            get { return _command; }
        }

        public TileInvoker(MacroCommand cmds)
        {
            _command = cmds;
        }

        /// <summary>
        /// 触发区块上的事件命令
        /// </summary>
        /// <param name="player">调用者</param>
        public void Invoke(IReceiver player)
        {
            _command.Execute(player);
        }

        public virtual void Render(int row, int col)
        {
            _command.Render();
        }

        public abstract bool CanPass { get; }

        public virtual void Update() { }
    }

    /// <summary>
    /// 空区块
    /// </summary>
    class EmptyTile : TileInvoker
    {
        public override void Render(int row, int col)
        {
            //渲染到界面
            base.Render(row, col);
        }

        public override bool CanPass
        {
            get { return true; }
        }
    }

    /// <summary>
    /// 动态区块,实现区块图像轮播的动画效果
    /// </summary>
    class DynamicTile : TileInvoker
    {
        //维护的数据对象

        private int _currentFrame = 0;

        public override void Render(int row, int col)
        {
            //动态渲染到界面
            base.Render(row, col);
        }

        public override bool CanPass
        {
            get { return false; }
        }

        public override void Update()
        {
            base.Update();

            //更新计数器
        }
    }

由上可见,Invoker具有自己的类层次,不受Command类层次的影响。封装了命令,意味着从Invoker中分离了次要变化,这里的MacroCommand又像是一个Bridge(连接桥)。


客户代码。Invoke方法由负责移动的程序调用。

    /// <summary>
    /// 区块地图,存储区块的集合 -- client
    /// </summary>
    class TileMap
    {
        private TileInvoker[,] _tileMap;

        public TileMap(int row, int col)
        {
            _tileMap = new TileInvoker[row, col];
        }

        public void Bulid()
        { 
            //根据配置文件创建并参数化区块对象     
        }

        public void Render()
        {
            foreach (var item in _tileMap)
            {
                item.Render();
            }
        }

        /// <summary>
        /// 触发指定位置上的区块事件
        /// </summary>
        public void Invoke(int row, int col, IReceiver player)
        {
            _tileMap[row, col].Invoke(player);
        }
    }


6. 总结


没想到一篇文章写了10多个小时,够我读完一本书了~~~~(>_<)~~~~ 
我也没什么设计经验,以上只是个人论点。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值