学习了Martin Fowler的重构以后,我就发现有必要学习一下设计模式。市面上有很多设计模式的书籍,我偏偏选择了GoF写的那本《设计模式》。这本书上的内容是难以想象的厚实,几乎涵盖了设计模式使用的所有细节和变化。因为我是新手,读这本书有一种惊心动魄的感觉,每一句话都随时有可能颠覆我的理解和认识。仅仅是从案例代码上学习到的知识,比很多经典教材来的都要多!
所以我打算写一篇关于设计模式的文章,来阐发或加深自己的理解,以比较难以掌握和理解的command模式为例。这不是一篇对command模式的教程,只是陈述我的个人认识,并追求将这一认识深化,以文档的形式明确下来。不保证我给出的案例完整呈现command模式的应用场合,也不保证command模式是这一问题的最佳解决方案。
1. 问题引入
1)概述:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。[GOF《设计模式》]command封装请求,使请求者与实现者松散耦合。
2. 我的认识
3. 案例引入
4. 方法选择
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. 参考代码
类静态图预览: /// <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);
}
}