游戏框架搭建

使用框架的目标:低耦合,高内聚,表现和数据分离
耦合:对象,类的双向引用,循环引用
内聚:相同类型的代码放在一起
表现和数据分离:需要共享的数据放在Model里

对象之间的交互一般有三种

  1. 方法调用,A持有B才能调用B的方法
  2. 委托或回调,A持有B才能注册B的委托,尽量避免嵌套调用
  3. 消息或事件,A不需要持有B

A调用B的方法,A就必须持有B,形成单向引用关系,为了避免耦合,B不应该引用A,如果B想调用A的方法,使用委托或回调。
总结:父节点调用子节点可以直接方法调用,子节点通知父节点用委托或事件,跨模块通信用事件

在这里插入图片描述

同级之间的单向依赖也要尽量避免

事件的好处就是降低耦合,两个对象不需要访问彼此,就可以进行交互。
坏处是事件的使用成本低,容易造成事件满天飞,导致代码可读性降低。其次,事件很容易破坏单向依赖的结构,事件的发送方和接收方不会受到任何限制,在使用事件时不容易掌握事件的使用范围。

结论:模块之间可以用事件,对象之间能不用事件就不用事件,事件所负责的逻辑,其颗粒度要尽量大一点。

委托

  • 使用范围最窄
  • 一般需要注册,注销成对出现
  • 需要访问对象
  • 容易产生回调地狱
  • 推荐自底向上单向依赖时使用

事件

  • 使用范围最广
  • 注册,注销成对出现
  • 不需要访问对象
  • 容易消息满天飞
  • 推荐模块之间,同级之间使用

模块化一般有三种

  1. 单例,例如: Manager Of Managers
  2. IOC,例如: Extenject,uFrame的 Container,StrangelOC的绑定等等
  3. 分层,例如: MVC、三层架构、领域驱动分层等等

交互逻辑和表现逻辑

在这里插入图片描述
以计数器为例,用户操作界面修改数据叫交互逻辑,当数据变更之后或者初始化时,从Model里查询数据在View上显示叫表现逻辑
交互逻辑:View -> Model
表现逻辑:Model -> View

很多时候,我们不会真的去用 MVC 开发架构,而是使用表现(View)和数据(Model)分离这样的思想,我们只要知道 View 和 Model 之间有两种逻辑,即交互逻辑 和 表现逻辑,我们就不用管中间到底是 Controller、还是 ViewModel、还是 Presenter。只需要想清楚交互逻辑 和 交互逻辑如何实现的就可以了。

View和Model怎样交互比较好,或者说交互逻辑和表现逻辑怎样实现比较好?

<1> 直接方法调用,表现逻辑是在交互逻辑完成之后主动调用,伪代码如下

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑
                CounterModel.Count++;
                
                // 表现逻辑
                UpdateView();
            });

        transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑
                CounterModel.Count--;
                
                // 表现逻辑
                UpdateView();
            });
        
        // 表现逻辑
        UpdateView();
    }

    void UpdateView()
    {
        transform.Find("CountText").GetComponent<Text>().text = CounterModel.Count.ToString();
    }
}

public static class CounterModel
{
    public static int Count = 0;
}

<2> 使用委托

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        // 注册
        CounterModel.OnCountChanged += OnCountChanged;

        transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count++;
            });

        transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count--;
            });
        
        OnCountChanged(CounterModel.Count);
    }

    // 表现逻辑
    private void OnCountChanged(int newCount)
    {
        transform.Find("CountText").GetComponent<Text>().text = newCount.ToString();
    }

    private void OnDestroy()
    {
        // 注销
        CounterModel.OnCountChanged -= OnCountChanged;
    }
}

public static class CounterModel
{
    private static int mCount = 0;

    public static event Action<int> OnCountChanged ;
    
    public static int Count
    {
        get => mCount;
        set
        {
            if (value != mCount)
            {
                mCount = value;
                OnCountChanged?.Invoke(value);
            }
        }
    }
}

<3> 使用事件,事件管理器写法差不多,这里忽略具体实现

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        // 注册
        EventManager.Instance.RegisterEvent(EventId, OnCountChanged);

        transform.Find("BtnAdd").GetComponent<Button>()
            .onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count++;
            });

        transform.Find("BtnSub").GetComponent<Button>()
            .onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count--;
            });
        
        OnCountChanged();
    }

    // 表现逻辑
    private void OnCountChanged()
    {
        transform.Find("CountText").GetComponent<Text>().text = CounterModel.Count.ToString();
    }

    private void OnDestroy()
    {
        // 注销
        EventManager.Instance.UnRegisterEvent(EventId, OnCountChanged);
    }
}

public static class CounterModel
{
    private static int mCount = 0;
    
    public static int Count
    {
        get => mCount;
        set
        {
            if (value != mCount)
            {
                mCount = value;
                
                // 触发事件
                EventManager.Instance.FireEvent(EventId);
            }
        }
    }
}

比较上面3种实现方式,当数据量很多的时候,使用第1种方法调用会写很多重复代码调用,代码臃肿,容易造成疏忽,使用委托或事件代码更精简,当数据变化时会自动触发表现逻辑,这就是所谓的数据驱动。

所以表现逻辑使用委托或事件更合适,如果是单个数值变化,用委托的方式更合适,比如金币、分数、等级、经验值等等,如果是颗粒度较大的更新用事件比较合适,比如从服务器拉取了一个任务列表数据,然后任务列表数据存到了Model

BindableProperty

上面的Model类,每新增一个数据就要写一遍类似的代码,很繁琐,我们使用泛型来简化代码

public class BindableProperty<T> where T : IEquatable<T>
{
    private T mValue;

    public T Value
    {
        get => mValue;
        set
        {
            if (!mValue.Equals(value))
            {
                mValue = value;
                OnValueChanged?.Invoke(value);
            }
        }
    }

    public Action<T> OnValueChanged;
}

BindableProperty 也就是可绑定的属性,是 数据 + 数据变更事件 的合体,它既存储了数据充当 C# 中的 属性这样的角色,也可以让别的地方监听它的数据变更事件,这样会减少大量的样板代码

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        // 注册
        CounterModel.Count.OnValueChanged += OnCountChanged;

        transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count.Value++;
            });

        transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count.Value--;
            });
        
        OnCountChanged(CounterModel.Count.Value);
    }

    // 表现逻辑
    private void OnCountChanged(int newValue)
    {
        transform.Find("CountText").GetComponent<Text>().text = newValue.ToString();
    }

    private void OnDestroy()
    {
        // 注销
        CounterModel.Count.OnValueChanged -= OnCountChanged;
    }
}

public static class CounterModel
{
    public static BindableProperty<int> Count = new BindableProperty<int>()
    {
        Value = 0
    };
}

总结:

  • 自顶向下的逻辑使用方法调用
  • 自底向上的逻辑使用委托或事件,Model和View是底层和上层的关系,所以用委托或事件更合适

Command

实际的开发中交互逻辑的代码是很多的,随着功能需求越来越多,Controller的代码会越来越臃肿,解决办法是引入命令模式(Command),命令模式参考另一篇博客:Unity常用设计模式
先定义一个接口

public interface ICommand
{
    void Execute();
}

添加一个命令,实现数据加一操作,注意这里是用 struct 实现的,而不是用的 class,这是因为游戏里边的交互逻辑有很多,如果每一个都用去 new 一个 class 的话,会造成很多性能消耗,比如 new 一个对象所需要的寻址操作、比如对象回收需要的 gc 等等,而 struct 内存管理效率要高很多

public struct AddCountCommand : ICommand
{
    public void Execute()
    {
        CounterModel.Count.Value++;
    }
}

实现数据减一操作

public struct SubCountCommand : ICommand
{
    public void Execute()
    {
        CounterModel.Count.Value--;
    }
}

更新交互逻辑的代码

    public class CounterViewController : MonoBehaviour
    {
        void Start()
        {
            // 注册
            CounterModel.Count.OnValueChanged += OnCountChanged;

            transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
                {
                    // 交互逻辑
                    new AddCountCommand().Execute();
                });

            transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
                {
                    // 交互逻辑
                    new SubCountCommand().Execute();
                });
            
            OnCountChanged(CounterModel.Count.Value);
        }

        // 表现逻辑
        private void OnCountChanged(int newValue)
        {
            transform.Find("CountText").GetComponent<Text>().text = newValue.ToString();
        }

        private void OnDestroy()
        {
            // 注销
            CounterModel.Count.OnValueChanged -= OnCountChanged;
        }
    }

    public static class CounterModel
    {
        public static BindableProperty<int> Count = new BindableProperty<int>()
        {
            Value = 0
        };
    }

使用 Command 符合读写分离原则(Comand Query Responsibility Segregation),简写为 CQRS ,这个概念在 StrangeIOC、uFrame、PureMVC、Loxodon Framework 都有实现,而在微服务领域比较火的 DDD(领域驱动设计)的实现一般也会实现 CQRS。它是一种软件架构模式,旨在将应用程序的读取和写入操作分离为不同的模型。在CQRS中,写操作通常由命令模型(Command Model)来处理,它负责处理业务逻辑和状态更改。而读操作则由查询模型(Query Model)来处理,它专门用于支持数据查询和读取展示。

Command 模式就是逻辑的调用和执行是分离的,我们知道一个方法的调用和执行是不分离的,因为一旦你调用方法了,方法也就执行了,而 Command 模式能够做到调用和执行在空间和时间上是能分离的。

空间分离的方法就是调用的地方和执行的地方放在两个文件里。
时间分离的方法就是调用的之后,Command 过了一点时间才被执行。

Command 分担 Controller 的交互逻辑,由于有了调用和执行分离这个特点,所以我们可以用不同的数据结构去组织 Command 调用,比如列表,队列,栈

在这里插入图片描述
底层系统层是可以共享给别的展现层使用的,切换表现层非常方便,表现层到系统层用 Command 改变底层系统的状态(数据),系统层通过事件或者委托通知表现层,在通知的时候可以推送数据,也可以让表现层收到通知后自己去查询数据。

模块化

使用单例

单例比静态类好一点就是其生命周期相对可控,而且访问单例对象比访问静态类多了一点限制,也就是需要通过 Instance 获取

每个模块继承 Singleton

public class Singleton<T> where T : class
{
    public static T Instance
    {
        get
        {
            if (mInstance == null)
            {
                // 通过反射获取构造
                var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
                // 获取无参非 public 的构造
                var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);

                if (ctor == null)
                {
                    throw new Exception("Non-Public Constructor() not found in " + typeof(T));
                }

                mInstance = ctor.Invoke(null) as T;
            }

            return mInstance;
        }
    }

    private static T mInstance;
}

问题:单例没有访问限制,容易造成模块之间互相引用,关系混乱

在设计一个单例时要尽量做到如下规则:

  1. 严格控制对外暴露的 API,与外界交互只通过静态方法。
  2. 严格控制单例的使用范围,不要让单例类的职责太多
  3. 严格控制单例对象的生命周期(创建、初始化、销毁)

IOC容器

IOC 容器可以理解为是一个字典,这个字典以 Type 为 key,以对象即 Instance 为 value,IOC 容器最少有两个核心的 API,即根据 Type 注册实例,根据 Type 获取实例

public class IOCContainer
{
    /// <summary>
    /// 实例
    /// </summary>
    public Dictionary<Type, object> mInstances = new Dictionary<Type, object>();

    /// <summary>
    /// 注册
    /// </summary>
    /// <param name="instance"></param>
    /// <typeparam name="T"></typeparam>
    public void Register<T>(T instance)
    {
        var key = typeof(T);

        if (mInstances.ContainsKey(key))
        {
            mInstances[key] = instance;
        }
        else
        {
            mInstances.Add(key,instance);
        }
    }

    /// <summary>
    /// 获取
    /// </summary>
    public T Get<T>() where T : class
    {
        var key = typeof(T);
        
        object retObj;
        
        if(mInstances.TryGetValue(key,out retObj))
        {
            return retObj as T;
        }

        return null;
    }
}

下面是一个简单的示例,IOC 容器创建,注册实际应当写在游戏初始化时,这里为了方便演示都写在一起了

public class IOCExample : MonoBehaviour
{
    void Start()
    {
        // 创建一个 IOC 容器
        var container = new IOCContainer();
        
        // 注册一个蓝牙管理器的实例
        container.Register(new BluetoothManager());
        
        // 根据类型获取蓝牙管理器的实例
        var bluetoothManager = container.Get<BluetoothManager>();
        
        //连接蓝牙
        bluetoothManager.Connect();
    }

    public class BluetoothManager
    {
        public void Connect()
        {
            Debug.Log("蓝牙连接成功");
        }
    }
}

为了避免样板代码,这里创建一个抽象类

/// <summary>
/// 架构
/// </summary>
public abstract class Architecture<T> where T : Architecture<T>, new()
{
    #region 类似单例模式 但是仅在内部课访问
    private static T mArchitecture = null;
    
    // 确保 Container 是有实例的
    static void MakeSureArchitecture()
    {
        if (mArchitecture == null)
        {
            mArchitecture = new T();
            mArchitecture.Init();
        }
    }
    #endregion

    private IOCContainer mContainer = new IOCContainer();

    // 留给子类注册模块
    protected abstract void Init();

    // 提供一个注册模块的 API
    public void Register<T>(T instance)
    {
        MakeSureArchitecture();
        mArchitecture.mContainer.Register<T>(instance);
    }

    // 提供一个获取模块的 API
    public static T Get<T>() where T : class
    {
        MakeSureArchitecture();
        return mArchitecture.mContainer.Get<T>();
    }
}

子类注册多个模块

public class PointGame : Architecture<PointGame>
{
    // 这里注册模块
    protected override void Init()
    {
        Register(new GameModel1());
        Register(new GameModel2());
        Register(new GameModel3());
        Register(new GameModel4());
    }
}

使用 IOC 容器的目的是增加模块访问的限制

除了可以用来注册和获取模块,IOC 容器一般还会有一个隐藏的功能,即:注册接口模块

public class IOCExample : MonoBehaviour
{
    void Start()
    {
        // 创建一个 IOC 容器
        var container = new IOCContainer();
        
        // 根据接口注册实例
        container.Register<IBluetoothManager>(new BluetoothManager());
        
        // 根据接口获取蓝牙管理器的实例
        var bluetoothManager = container.Get<IBluetoothManager>();
        
        //连接蓝牙
        bluetoothManager.Connect();
    }
    
    /// <summary>
    /// 定义接口
    /// </summary>
    public interface IBluetoothManager
    {
        void Connect();
    }

    /// <summary>
    /// 实现接口
    /// </summary>
    public class BluetoothManager : IBluetoothManager
    {
        public void Connect()
        {
            Debug.Log("蓝牙连接成功");
        }
    }
}

抽象-实现 这种形式注册和获取对象的方式是符合依赖倒置原则的。
依赖倒置原则(Dependence Inversion Principle):程序要依赖于抽象接口,不要依赖于具体实现。依赖倒置原则是 SOLID 中的字母 D。

这种设计的好处:

  • 接口设计与实现分成两个步骤,接口设计时可以专注于设计,实现时可以专注于实现。
  • 实现是可以替换的,比如一个接口叫 IStorage,其实现可以是 PlayerPrefsStorage、EdtiroPrefsStorage,等切换时候只需要一行代码就可以切换了。
  • 比较容易测试(单元测试等)
  • 降低耦合。
接口的显式实现
public interface ICanSayHello
{
    void SayHello();
    void SayOther();
}

public class InterfaceDesignExample : MonoBehaviour, ICanSayHello
{
    /// <summary>
    /// 接口的隐式实现
    /// </summary>
    public void SayHello()
    {
        Debug.Log("Hello");
    }

    /// <summary>
    /// 接口的显式实现,不能写访问权限关键字
    /// </summary>
    void ICanSayHello.SayOther()
    {
        Debug.Log("Other");
    }
    
    void Start()
    {
        // 隐式实现的方法可以直接通过对象调用
        this.SayHello();
        
        // 显式实现的接口不能通过对象调用
        // this.SayOther() // 会报编译错误
        
        // 显式实现的接口必须通过接口对象调用
        (this as ICanSayHello).SayOther();
    }
}

当需要实现多个签名一致的方法时,可以通过接口的显式声明来区分到底哪个方法是属于哪个接口的
利用接口的显示实现,子类想要调用必须先转成接口,这样就增加了调用显式实现的方法的成本,所以可以理解为这个方法被阉割了

分层

前面使用 Command 分担了 Controller 的交互逻辑的部分逻辑,并不是所有的交互逻辑都适合用 Command 来分担的,还有一部分交互逻辑是需要交给 System 层来分担。这里 System 层在概念等价于游戏的各个管理类 Manager。
Command 是没有状态的,有没有状态我们可以理解为这个对象需不需要维护数据,因为 Command 类似于是一个方法,只要调用然后执行一次就可以不用了,所以 Command 是没有状态的

梳理一下当前的架构

  • 表现层:即 ViewController 或者 MonoBehaviour 脚本等,负责接受用户的输入,当状态变化时更新表现
  • System 层:系统层,有状态,在多个表现层共享的逻辑,负责即提供 API 又有状态的对象,比如网络服务、蓝牙服务、商城系统等,也支持分数统计、成就系统这种硬编码比较多又需要把代码放在一个位置的需求。
  • Model 层:管理数据,有状态,提供数据的增删改查。
  • Utility 层:工具层,无状态,提供一些必备的基础工具,比如数据存储、网络链接、蓝牙、序列化反序列化等。

表现层改变 System、Model 层级的状态用 Command,System 层 和 Model 层 通知 表现层用事件,委托或 BindableProeprty,表现层查询状态时可以直接获取 System 和 Model 层

每个层级都有一些规则:

表现层

  • 可以获取 System
  • 可以获取 Model
  • 可以发送 Command
  • 可以监听 Event

系统层

  • 可以获取其他 System
  • 可以获取 Model
  • 可以监听,发送 Event
  • 可以获取 Utility

数据层

  • 可以获取 Utility
  • 可以发送 Event

工具层

  • 啥都干不了,可以集成第三方库,或者封装 API

除了四个层级,还有一个核心概念就是 Command

Command

  • 可以获取 System
  • 可以获取 Model
  • 可以获取 Utility
  • 可以发送 Event
  • 可以发送其他 Command

贫血模型和充血模型

在这里插入图片描述
我们有一个 User 对象,伪代码如下

public class User
{
    public string Name {get;set;}
    public int Age {get;set;}
    public string Id {get;set;}
    public string NickName {get;set;
    public float Weight {get;set;}
}

总共有五个属性,但是在表现层的界面中,只需要显示三个属性,即:姓名 Name、年龄 Age、和 Id。
表现层查询一个用户数据的时候,返回了一个 完整的 User 对象,这种数据流向模型叫做贫血模型。就是表现层需要用到的数据结果给了一整个未经过筛选的数据对象过来。

在这里插入图片描述
定义了一个 UserInfo 类,伪代码如下;

public class UserInfo
{
    public string Name {get;set;}
    public int Age {get;set;}
    public string Id {get;set;}
}

充血模型就是表现层需要哪些数据,就刚好返回哪些数据

充血模型 比 贫血模型 需要做跟多的工作,写更多的代码,甚至还有跟多的性能消耗。
但是在越大规模的项目中 充血模型 的好处就会更加明显。因为充血模型,可以让我们的代码更精确地描述业务,会提高代码的可读性,而贫血模型,会让我们的数据逐渐趋于混乱。

控制反转,依赖注入,依赖倒置原则

控制反转(IOC,Invertion Of Control)是一种设计思想,而不是一个具体的技术实现。它将传统上由程序代码直接操控的对象调用权交给外部容器或框架来管理,实现对象之间的解耦。

依赖注入(DI,Dependency Injection)是 IOC 思想的一种实现。

public class A
{
	public B b;
}

public class B {}

void Main()
{
	A a = new A();
	B b = new B();
	a.b = b;
}

依赖其实就是持有对象,上面代码中 a 持有 b 对象,即 a 依赖 b,说明 a 需要 b 才能完成自身的职责,a.b = b 就是注入的过程,注入可以理解为设置值,通过方法或构造函数传递对象也算注入。

使用 DI 方案的时候,离不开 DI 容器这个概念。有的时候 DI 容器 也叫做 IOC 容器,即:DIContainer 和 IOCContainer,它俩指的是一个概念。DI 容器的职责很简单,就是管理依赖和注入依赖,而管理依赖是通过类型来管理依赖。注入依赖就是向一个对象注入实例。

在 DIContainer 的具体实现中,依赖是一个以类型为 key,以实例为 value 的一个 key-value 结构,所以一般的 DI Container 会用一个 Dictionary<Type,object> 来作为核心数据结构。即根据 Type 可以得到 Type 的实例,这个依赖,不是说 Type 依赖 object,而是说 Type:object 这个结构整体就是一个依赖 。这依赖是相对于要注入的对象的,即它们是 Inject(object obj) 中的 obj 的依赖。

一般情况下,DI 容器会提供如下的 API:

  • 注册类型:Register<TSource,TTarget>
  • 注入: Inject(object obj)
  • 解析: Resolve<T>(),根据类型返回实例,就是Get方法,可以每次返回新对象,也可以每次返回同一个对象,看具体实现

参考代码 IOCKit

单例结构,有依赖关系的结构,DIContainer对比

在这里插入图片描述

使用单例时,项目结构的设计非常依赖于约定(文档等等)。
但是单例的获访问没有限制,难以表达约定上的分层的那些概念,因为只要一个模块变成单例了,那么这个模块就在技术上失去了层次关系。一旦项目规模变大,模块数量变多,那么约定就会越来越难以遵循,就会非常容易导致项目结构的崩溃。

在这里插入图片描述

最顶层的模块都用单例,然后一些底层模块,作为顶层模块的成员变量,从而达到逻辑层无法直接访问底层模块,而是必须通过顶层模块间接地使用底层模块的服务。

这样就解决了单例结构无法表达层级的问题,但是同时也失去了单例带来的种种好处,比如易扩展,维护性(模块独立)。现在不容易扩展模块了,因为要扩展模块需要考虑依赖关系,也不容易维护了,因为模块不独立了。这里不容易维护指的是各个模块不容易单独维护了,但是整体项目会更容易维护。

在这里插入图片描述

使用 DI Container

public class ModuleA
{
  // 声明需要注入的对象
  [Inject]
  public ModuleB ModuleB {get;set;}
  
  void XXX()
  {
    ModuleB.DoSomeThing(); // 放心用,因为不用考虑依赖的创建过程。
  }
}

启动时,需要统一注册依赖

public static QFrameworkContainer Container {get;private set;}

void Main()
{
  //创建实例容器
  Container = new Contaner();
  // 注册类型
  Container.Register<ModuleB>();
}

创建 ModuleA 对象时需要注入依赖

void MethodA()
{
  // 注入对象,先找到 ModuleA 中带有 [Inject] 标记的变量 ModuleB,从容器中查找是否存在 ModuleB 类型,
  // 有就会创建一个 ModuleB 对象,然后将其赋值给类型为 ModuleB 的成员变量
  App.Container.Inject(new ModuleA());
}

只需要在模块内部声明其他模块作为变量,而不需要考虑创建过程,这样就导致每个模块都是可独立测试的(易维护),而扩展一个模块,注册到类型容器即可,这样的话扩展一个模块比单例还容易。

总结

单例结构容易维护和扩展,访问无限制,无法表达层级结构,只能靠约定设计结构
有依赖关系的结构不易维护和扩展,访问有限制,可以表达层级
DIContainer易维护和扩展,可以表达层级,可以让我们设计依赖关系

生命周期

生命周期(也称为 Scope 即 域),指的是 DIContainer 的注入实例的生命周期
即当 DIContainer 注入实例时,所注入实例的是新创建还是复用同一个实例?

在 QFramework DIContainer 方案中是这样的。

  • 当注册依赖时,使用 Register() 方法时,每次注入都会创建新的实例,一般情况下管这种方式方式叫做 Transient(短暂的)。
  • 当注册依赖时,使用 RegisterInstance(object obj) 方法时,每次注入都会使用 obj 实例,一般情况下管这种方式叫做 Singleton(单例)。

依赖倒置原则

依赖倒置原则,即:Dependency Inversion Principle,很多时候缩写成 DIP。
这个原则包含了两个主题:

  • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象概念。
  • 抽象接口不应该依赖于实现,而实现依赖于抽象接口
public class Driver
{
  public void Drive(Benz benz)
  {
    benz.Run();
  }
}

public class Benz
{
  public void Run()
  {
    Debug.Log("奔驰汽车开始运行")
  }
}

void Main()
{
  var lucas = new Driver();
  var benz = new Benz();
  lucas.Drive(benz);
}

上诉代码以司机开车为例,Driver 是高层模块,Benz 是底层模块,Driver 依赖于 Benz,后续如果要扩展新的类型,Driver 类也需要修改,这就说明 Driver 类与 Benz 类耦合太强了。

public class Driver
{
  public void Drive(ICar car)
  {
    car.Run();
  }
}

public interface ICar
{
  void Run();
}

public class Benz : ICar
{
  public void Run()
  {
    Debug.Log("奔驰汽车开始运行")
  }
}

public class BMW : ICar
{
  public void Run()
  {
    Debug.Log("宝马汽车开始运行")
  }
}

void Main()
{
  var lucas = new Driver();
  var bmw = new BMW();
  lucas.Drive(bmw);
}

增加一个 ICar 接口,Driver 不依赖某个具体的车, 而依赖 ICar,而每个车实现 ICar 接口,这样就减少耦合。实际上 Driver 类也应该抽象出来一个 IDriver,不过对于目前的结构来说没有必要。

ICar car = new BMW();

抽象接口不应该依赖于实现,而实现依赖于抽象接口,ICar 不应该依赖于 BMW,而 BMW 依赖于 ICar

依赖倒置原则的两个主题都在说一件事情,两个类之间的交互通过抽象(接口、抽象类)来完成。而通过一个抽象,可以将两个类的变化隔离。即 A 类修改或扩展甚至是替换都不影响 B,反过来也是如此。

通过 DIContainer 实现 DIP

依赖 = 需要,这个需要包含了很多关系,即实现接口是依赖关系,持有对象也是依赖关系

public interface ILoginService
{
  void Login();
}

public class LoginService : ILoginService
{
  public void Login()
  {
    Debug.Log("登录成功");
  }
}

public class IOCInterfaceExample : MonoBehaviour 
{
  [Inject]
  public ILoginService LoginService {get;set;}

  void Start () 
  {
    var container = new QFrameworkContainer();
  
    container.RegisterInstance<ILoginService>(new LoginService());
    
    // Register 也支持注册接口依赖
    //container.Register<ILoginService,LoginService>();
  
    container.Inject(this);

    LoginService.Login();
  }
}

根据接口注入具体的实例

控制反转

通过接触依赖倒置原则,依赖注入方案后,我们理解控制反转会更容易一点

public class ModuleA
{
  public ModuleB B = new ModuleB();
}

public class ModuleB {}

有控制反转,就有控制正转,上述代码中,ModuleB 对象的创建过程在 ModuleA 内部完成,这就是控制正转,或者说正常的控制过程,因为没有正转这个词,这里控制意思是依赖对象创建过程的控制。

public class ModuleA
{
  public ModuleB B;
}

public class ModuleB {}

void Main()
{
  var moduleA = new ModuleA();
  // 对象创建过程由外部控制
  moduleA.B = new ModuleB();
}

控制反转就是 B 对象的创建过程由外部控制,即依赖对象的创建过程交给外部控制。

参考

凉鞋 《框架搭建 决定版》

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值