ETJava Beta | Java    注册   登录
  • 搜索:
  • 架构演化思考总结(2)

    发表于      阅读(1)     博客类别:Crawler     转自:https://www.cnblogs.com/TonyCode/p/18335588
    如有侵权 请联系我们删除  (页面底部联系我们)  

    架构演化思考总结(2)

    —-–从命令模式中来探索处理依赖关系

    在正式引入命令模式的概念之前,我们先从简单的案例来逐步演化大家在书面上常见到的内容。

    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对命令模式的介绍:
    image

    上面小案例正对应对“操作逻辑”进行封装,提炼成命令,那么这样对操作逻辑进行封装有什么好处呢?

    显而易见的好处之一就是方便管理,逻辑清晰。进行复杂逻辑开发时候,我们正式把它尽可能提炼封装成方法,为的就是方便管理,而命令模式是对逻辑代码再高一个层次的封装,也就是说从方法抽象成类,显然更加便于管理和复用。

    使用命令模式降低复杂逻辑的开发调试难度,你排查一个几百行的大函数的bug肯定比封装拆分成几个函数或者是几个对应的命令的状况要麻烦。比如我们需要进行某个复杂操作,但是我们对它进行拆分封装,分成几个Command来执行,这样既可以分发给几个同事一起协作复杂逻辑开发,没有与核心逻辑控制脚本产生过多的耦合

    当然另一方面分担了控制脚本Controller的控制压力,使其没那么臃肿。

    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());
    }
    

    命令模式–携带参数

    如果我们要执行需要参数才能进行的命令呢?

    好,接下来我们实现可以携带参数的命令,非常简单,只需要给执行的命令中声明参数即可!

    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的逐步撤销功能了!

    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();
        }
    }
    

    命令模式–命令和执行分离

    这里和上一篇所陈述的依赖关系大致相同,我们把命令从一个对象降级成方法来看。

    我们常常进行的方法调用这种行为,就是命令和执行未分离的一个例子。即方法调用必然方法中的逻辑执行。

    void DoSomethingCommand()
    {
        Debug.Log("命令执行了!");
    }
    void Start()
    {
        DoSomethingCommand();
    }
    

    那么命令和执行分开是怎么样的呢?

    我们可以使用委托来实现,时间和空间上的分离。

     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之后,要注销对事件的监听功能。

    现在按照这样的要求来实现接口:

     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();
     }
    

    来着重实现自动注销机制:

    我们来声明一个类,来具体执行注销事件的功能:

    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时候进行触发。

    来实现对应的触发器:

    /// <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上。

    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中委托的注册和注销成对出现的特征,防止委托中出现空指针。

    好,实现完成自动注销事件机制,继续实现事件的注册和调用机制。

    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的时空分离:

    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模式!

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