架构演化思考总结(2)
------从命令模式中来探索处理依赖关系
在正式引入命令模式的概念之前,我们先从简单的案例来逐步演化大家在书面上常见到的内容。
C#
public interface ICommand
{
void Execute();
}
public class PlayMusicCommand : ICommand
{
public void Execute()
{
Debug.Log("你说家是唯一的城堡,随着稻香一路奔跑~");
}
}
var Start()
{
var command = new PlayMusicCommand();
command.Execute();
}
这里我们定义一个命令接口,如果是命令,必定要实现的一个执行方法。
PlayMusicCommand 实现接口,此命令的作用就是播放Jay的《稻香》,如果想实现播放音乐功能,直接执行对应命令的方法即可!
体现出命令的本质,我们把要所作的内容或者控制逻辑封装到一起,当我们需要执行它时候,下达执行命令的方法即可!
来看AI对命令模式的介绍:
上面小案例正对应对"操作逻辑"进行封装,提炼成命令,那么这样对操作逻辑进行封装有什么好处呢?
显而易见的好处之一就是方便管理,逻辑清晰。进行复杂逻辑开发时候,我们正式把它尽可能提炼封装成方法,为的就是方便管理,而命令模式是对逻辑代码再高一个层次的封装,也就是说从方法抽象成类,显然更加便于管理和复用。
使用命令模式降低复杂逻辑的开发调试难度 ,你排查一个几百行的大函数的bug肯定比封装拆分成几个函数或者是几个对应的命令的状况要麻烦。比如我们需要进行某个复杂操作,但是我们对它进行拆分封装,分成几个Command来执行,这样既可以分发给几个同事一起协作复杂逻辑开发,没有与核心逻辑控制脚本产生过多的耦合。
当然另一方面分担了控制脚本Controller的控制压力,使其没那么臃肿。
C#
public interface ICommand
{
void Execute();
}
public class ACommand
{
public void Execute()
{
Debug.Log("Execute A Command");
}
}
public class BCommand
{
public void Execute()
{
Debug.Log("Execute B Command");
}
}
public class CCommand
{
public void Execute()
{
Debug.Log("Execute C Command");
}
}
void Start()
{
var commands = new List<ICommand>();
commands.Add(new ACommand());
commands.Add(new BCommand());
commands.Add(new CCommand());
commands.ForEach(c=>c.Execute());
}
命令模式--携带参数
如果我们要执行需要参数才能进行的命令呢?
好,接下来我们实现可以携带参数的命令,非常简单,只需要给执行的命令中声明参数即可!
C#
interface ICommand
{
void Execute();
}
public class BuyGoodsCommand : ICommand
{
private int goodsId;
private int goodsCount;
public BuyGoodsCommand(int id,int count)
{
goodsId = id;
goodsCount = count;
}
public void Execute()
{
Debug.Log($"购买了id为{goodsId}的商品{goodsCount}个");
//执行相关的购买逻辑
//......
}
}
public class Test : MonoBehaviour
{
private void Start()
{
var buyGoodsCommand = new BuyGoodsCommand(1, 15);
buyGoodsCommand.Execute();
}
}
命令模式--撤销功能
接下来接着向命令模式的功能实现迈进,在刚接触命令模式的时候,会好奇的想到,既然把命令都封装好一步步执行了,那能不能撤销已经执行好的行为呢?笔者也是在学习到命令模式之后才联想到各种编辑工具的Ctrl+Z的效果的实现思路。那我们往命令模式中添加一个撤销功能。
当然要执行撤销命令,要有个容器来存储已经执行的命令,这里使用的是List,也可以用Stack
和Queue,当然使用栈就可以实现Ctrl + Z的逐步撤销功能了!
C#
interface ICommand
{
void Execute();
void Undo();
}
public class BuyGoodsCommand : ICommand
{
private int goodsId;
private int goodsCount;
public BuyGoodsCommand(int id,int count)
{
goodsId = id;
goodsCount = count;
}
public void Execute()
{
Debug.Log($"购买了id为{goodsId}的商品{goodsCount}个");
//执行相关的购买逻辑
//......
}
public void Undo()
{
Debug.Log($"刚才购买的id为{goodsId}的商品{goodsCount}个,已经全部退货!");
//执行相关的退货操作
//如库存++
//玩家金币++
}
}
public class Test : MonoBehaviour
{
private void Start()
{
var commands = new List<BuyGoodsCommand>();
commands.Add(new BuyGoodsCommand(1, 15));
commands.Add(new BuyGoodsCommand(5, 2));
//执行购买
commands.ForEach(command => command.Execute());
//5号物品不想要了 退货
commands[1].Undo();
}
}
命令模式--命令和执行分离
这里和上一篇所陈述的依赖关系大致相同,我们把命令从一个对象降级成方法来看。
我们常常进行的方法调用这种行为,就是命令和执行未分离的一个例子。即方法调用必然方法中的逻辑执行。
C#
void DoSomethingCommand()
{
Debug.Log("命令执行了!");
}
void Start()
{
DoSomethingCommand();
}
那么命令和执行分开是怎么样的呢?
我们可以使用委托来实现,时间和空间上的分离。
C#
public class A : MonoBehaviour
{
B b;
void Start()
{
b = transform.Find("Animation").GetComponent<B>();
// 注册完成的事件
b.OnDoSomethingDone += DoSomethingCommand;
}
void DoSomethingCommand()
{
Debug.Log("命令执行了!");
}
}
public class B : MonoBehaviour
{
// 定义委托
public Action OnDoSomethingDone = ()=>{};
//当动画播放完毕后调用
public void DoSomething()
{
//触发委托中的函数执行
OnDoSomethingDone();
}
}
这样将要执行的命令DoSomethingCommand,会在特定时机(时间上分离)由另外一个脚本(空间上分离)调用执行,实现时空分离。
好,我们已经在方法层面表述出命令的分离,现在我们回到类这个层面,将Command的声明和执行进行分离。
这就需要一个对委托进行另一层的封装使用,这里是用委托(可以简单理解为函数容器),存储的是函数(command简化为方法层面),可以使用。对应的将命令升级升级成对象,为此也要对
委托进行"升级",这里参考QFramWork的自定义的事件机制。
自定义事件机制
我们希望它事件机制拥有功能:发送事件功能和自动注销功能。
发送事件是必须的,而自动注销功能要的是当注册事件监听的GameObject的对象Destroy之后,要注销对事件的监听功能。
现在按照这样的要求来实现接口:
C#
public interface ITypeEventSystem
{
/// <summary>
/// 发送事件
/// </summary>
/// <typeparam name="T"></typeparam>
void Send<T>() where T : new ();
void Send<T>(T e);
IUnRegister Register<T>(Action<T> onEvent);
/// <summary>
/// 注销事件
/// </summary>
/// <param name="onEvent"></param>
/// <typeparam name="T"></typeparam>
void UnRegister<T>(Action<T> onEvent);
}
//注销机制
public interface IUnRegister
{
void UnRegister();
}
来着重实现自动注销机制:
我们来声明一个类,来具体执行注销事件的功能:
C#
public class TypeEventSystemUnRegister<T> : IUnRegister
{
//持有事件机制引用
public ITypeEventSystem TypeEventSystem { get; set; }
//持有待注销的委托
public Action<T> OnEvent {get;set;}
//具体的注销机方法
public void UnRegister()
{
//具体就是调用事件机制(系统)对应的方法,注销掉指定的函数 (OnEvent)
TypeEventSystem.UnRegister(OnEvent);
TypeEventSystem = null;
OnEvent = null;
}
}
当然注销时机是在当GameObjet销毁时候,为此需要一个"触发器",其挂载在注册事件的GameObject上,当检测到Destroy时候进行触发。
来实现对应的触发器:
C#
/// <summary>
/// 注销事件的触发器
/// </summary>
public class UnRegisterOnDestroyTrigger : MonoBehaviour
{
private HashSet<IUnRegister> mUnRegisters = new HashSet<IUnRegister>();
public void AddUnRegister(IUnRegister unRegister)
{
mUnRegisters.Add(unRegister);
}
private void OnDestroy()
{
foreach (var unRegister in mUnRegisters)
{
unRegister.UnRegister();
}
mUnRegisters.Clear();
}
}
来对注销机制的接口拓展功能,方便在注册事件时候调用一个方法,通过这个方法调用直接将上段代码所示的注销机制的触发器挂载在GameObject上。
C#
public static class UnRegisterExtension
{
public static void UnRegisterWhenGameObjectDestroyed(this IUnRegister unRegister, GameObject gameObject)
{
var trigger = gameObject.GetComponent<UnRegisterOnDestroyTrigger>();
if (!trigger)
{
trigger = gameObject.AddComponent<UnRegisterOnDestroyTrigger>();
}
trigger.AddUnRegister(unRegister);
}
}
至此,当我们在使用时候调用一下'UnRegisterWhenGameObjectDestroyed'方法,将会挂载Tirgger,当物体销毁时候会触发,实现自动注销事件,有效的保证了在使用Unity中委托的注册和注销成对出现的特征,防止委托中出现空指针。
好,实现完成自动注销事件机制,继续实现事件的注册和调用机制。
C#
public class TypeEventSystem : ITypeEventSystem
{
//使用依赖倒转原则
interface IRegistrations
{
}
class Registrations<T> : IRegistrations
{
public Action<T> OnEvent = obj => { };
}
//根据事件的类型来存储 对应的事件Action<T> 被封装成类 以接口类型存储
private Dictionary<Type, IRegistrations> mEventRegistrations = new Dictionary<Type, IRegistrations>();
public void Send<T>() where T : new()
{
var e = new T();
Send<T>(e);
}
//具体发送机制 调用机制
public void Send<T>(T e)
{
var type = typeof(T);
IRegistrations eventRegistrations;
if (mEventRegistrations.TryGetValue(type, out eventRegistrations))
{
//具体调用 "解压 降维" 调用委托
(eventRegistrations as Registrations<T>)?.OnEvent.Invoke(e);
}
}
//注册实现
public IUnRegister Register<T>(Action<T> onEvent)
{
var type = typeof(T);
//具体存储 "加压 升维" 向委托中添加函数
IRegistrations eventRegistrations;
//判断存储的事件类型存在否
if (mEventRegistrations.TryGetValue(type, out eventRegistrations))
{
}
else
{
//不存在就添加一个
eventRegistrations = new Registrations<T>();
mEventRegistrations.Add(type,eventRegistrations);
}
//如果存在就 解压 添加到"解压"好的事件机制中
(eventRegistrations as Registrations<T>).OnEvent += onEvent;
//返回注销对象需要的数据(引用)实例
// 可以不通过构造函数来对共有访问对象初始化赋值
return new TypeEventSystemUnRegister<T>()
{
OnEvent = onEvent,
TypeEventSystem = this
};
}
//注销方法的具体实现
public void UnRegister<T>(Action<T> onEvent)
{
var type = typeof(T);
IRegistrations eventRegistrations;
if (mEventRegistrations.TryGetValue(type,out eventRegistrations))
{
(eventRegistrations as Registrations<T>).OnEvent -= onEvent;
}
}
}
至此自定义的事件机制实现完毕!
如果感兴趣,关注其对应的测试案例展示,(将在单独一篇博客介绍此事件机制)
继续推进,使用此机制来实现Command的时空分离:
C#
public interface ICommand
{
void Execute();
}
public class SayHelloCommand
{
public void Execute()
{
// 执行
Debug.Log("Say Hello");
}
}
void Start()
{
// 命令
var command = new SayHelloCommand();
command.Execute();
mTypeEventSystem = new TypeEventSystem();
mTypeEventSystem.Register<ICommand>(Execute).UnRegisterWhenGameObjectDestroyed(gameObject);
// 命令 使用Command对象注册
mTypeEventSystem.Send<Icommand>(new SayHelloCommand());
}
那么对比三种实现方式发现什么?
- 方法:调用即执行!没有分离
- 事件机制:执行在事件注册中实现 有分离
- Command:执行在Command内部实现 有分离
显然,Command对命令和执行的分离程度介于方法和事件机制之间。
重点对比事件机制,在实现自定义方法之前,笔者已经点到委托存储的方法,和在使用封装后委托(自定义事件)可以存储类(命令实例),虽然都是通过委托来存储执行方法,使用Command更为自由一些,可以在自定义的位置和时机执行,而事件机制一般至少需要通过两个对象才能完整使用。
先写到这里吧!
下面我们继续探索命令模式在架构演化中的作用,继续接近我们学习中接触到的Command模式!
谢谢各位能和我一起来探索项目架构设计演化!