架构演化思考总结(2)

架构演化思考总结(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模式!

谢谢各位能和我一起来探索项目架构设计演化!