首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

架构演化思考总结(2)

编程知识
2024年07月31日 20:52

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

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

From:https://www.cnblogs.com/TonyCode/p/18335588
本文地址: http://www.shuzixingkong.net/article/642
0评论
提交 加载更多评论
其他文章 RPC和 HTTP协议
RPC 和HTTP 的区别 服务发现 HTTP,知道服务域名,可以通过 DNS 解析 得到 服务的IP地址,从而进行访问 RPC 需要一个专门的中间服务去保存服务名和IP信息(注册中心,nacos、consul),想要访问某个服务,就得同时注册到 中间服务,然后获取需要顶用服务的IP 和端口信息 底
全网最适合入门的面向对象编程教程:29 类和对象的Python实现-断言与防御性编程和help函数的使用
在Python中,断言是一种常用的调试工具,它允许程序员编写一条检查某个条件。本文主要介绍了断言的应用场景和特点以及assert语句的使用,同时介绍了防御性编程和help()函数。
全网最适合入门的面向对象编程教程:29 类和对象的Python实现-断言与防御性编程和help函数的使用 全网最适合入门的面向对象编程教程:29 类和对象的Python实现-断言与防御性编程和help函数的使用 全网最适合入门的面向对象编程教程:29 类和对象的Python实现-断言与防御性编程和help函数的使用
SSH Exporter:基于Prometheus的远程系统性能监控神器
SSH Exporter English | 中文 介绍 SSH Exporter 是一个基于 Prometheus 规范的监控工具,通过 SSH 协议远程收集目标服务器的系统性能数据,如 CPU 使用率、内存使用情况、磁盘和网络 I/O 等,并将这些数据暴露为 Prometheus 格式的 met
《最新出炉》系列初窥篇-Python+Playwright自动化测试-60 - 判断元素是否显示 - 下篇
1.简介 有些页面元素的生命周期如同流星一闪,昙花一现。我们也不知道这个元素在没在页面中出现过,为了捕获这一美好瞬间,让其成为永恒。我们就来判断元素是否显示出现过。 在操作元素之前,可以先判断元素的状态。判断元素操作状态也可以用于断言。 2.常用的元素判断方法 2.1page对象调用的判断方法 pa
《最新出炉》系列初窥篇-Python+Playwright自动化测试-60 - 判断元素是否显示 - 下篇 《最新出炉》系列初窥篇-Python+Playwright自动化测试-60 - 判断元素是否显示 - 下篇 《最新出炉》系列初窥篇-Python+Playwright自动化测试-60 - 判断元素是否显示 - 下篇
从C++看C#托管内存与非托管内存
进程的内存 一个exe文件,在没有运行时,其磁盘存储空间格式为函数代码段+全局变量段。加载为内存后,其进程内存模式增加为函数代码段+全局变量段+函数调用栈+堆区。我们重点讨论堆区。 进程内存 函数代码段 全局变量段 函数调用栈 堆区 托管堆与非托管堆 C# int a=10这种代码申请的内存空间位于
从C++看C#托管内存与非托管内存 从C++看C#托管内存与非托管内存 从C++看C#托管内存与非托管内存
代码随想录Day1
704.二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 示例 1: 输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释:
代码随想录Day1 代码随想录Day1 代码随想录Day1
在 Hub 上使用 Presidio 进行自动 PII 检测实验
我们在 Hugging Face Hub 上托管的机器学习 (ML) 数据集中发现了一个引人关注的现象: 包含个人未经记录的私密信息。这一现象为机器学习从业者带来了一些特殊挑战。 在本篇博客中,我们将深入探讨含有一种称为个人识别信息 (PII) 的私密信息的各类数据集,分析这些数据集存在的问题,并介
在 Hub 上使用 Presidio 进行自动 PII 检测实验
我用Awesome-Graphs看论文:解读GraphBolt
这次向大家分享一篇流图处理系统论文GraphBolt,看如何基于计算历史的方式实现增量图计算,并保证与全量图计算语义的一致性。
我用Awesome-Graphs看论文:解读GraphBolt 我用Awesome-Graphs看论文:解读GraphBolt 我用Awesome-Graphs看论文:解读GraphBolt