强大的依赖注入框架 StrangeIoC

发布时间:2023-05-19

StrangeIoC 框架的概述

StrangeIoC 是用于 Unity3D 游戏引擎的依赖注入框架,可以帮助 Unity 开发者更好的构建应用程序。这个框架以依赖注入和解耦为目标来设计,主要由三个部分组成:Bind、Injection、CrossContext。

1. Bind

Bind 是这个框架的核心部分,它是为了构建绑定关系的。当你需要将某些类型绑定到另外一些类型时,Bind 就是你需要使用的工具。Bind 是由一个 Binder 类型来掌控的。而 Binder 又用一个 Binding 类来表达绑定信息,它们之间的关系如下图所示: Binder和Binding的关系 在图中,你可以看到 Binder 中包含了一个 Binding 的字典,Binding 表示了一个绑定关系。对于一个类型来说,它可以绑定到多个 Binding 上,但每个 Binding 只能被一个类型绑定。在使用 Bind 进行绑定时,通常需要指定一个绑定的键值,这个键值可以是一个类型、字符串或对象等,但必须保证唯一。

2. Injection

Injection 即插入,是这个框架的第二个部分。它主要是通过依赖注入的方式向对象中注入所需的依赖。你可以通过两种方式来进行注入:属性注入和构造函数注入。

属性注入:

在类中定义属性,并用 [Inject] 标记标记需要注入的属性,StrangeIoC 将会在对象被注入时将绑定的类型实例自动注入到它们的对象中。

public class SomeComponent : MonoBehaviour
{
    [Inject]
    public SomeService SomeService { get; set; }
    // ...
}

构造函数注入:

在类的构造函数中加入依赖参数,并用 [Inject] 标记标记需要注入的参数类型,StrangeIoC 将会在对象被注入时将绑定的类型实例自动注入到它们的对象中。

public class SomeComponent : MonoBehaviour
{
    private readonly SomeService _someService;
    [Inject]
    public SomeComponent(SomeService someService)
    {
        _someService = someService;
    }
    // ...
}

3. CrossContext

CrossContext 处理的是跨上下文的依赖关系,在应用程序中设计良好的上下文管理可以大幅度提高应用程序的性能。StrangeIoC 框架为上下文管理提供了很好的支持,当需要处理不同上下文之间的依赖关系时,只需要导入 CrossContext,即可实现上下文间的交互。

StrangeIoC 的使用方法

下面我们来看一下 StrangeIoC 的实际使用方法。

1. 安装

在 Unity 的 Package Manager 中选择 Add package from git URL 并输入 git@github.com:strangeioc/strangeioc.git 即可安装。

2. 绑定类型

在使用 StrangeIoC 进行绑定之前,需要确认以下几点:

  1. 被绑定的类型必须是已经在场景中或预制体中存在的。
  2. 绑定是全局的,不同的上下文之间共享绑定关系。
  3. 使用构造函数注入时,你需要确保类型的所有依赖都也进行了绑定。
using strange.extensions.context.impl;
using strange.extensions.injector.api;
using UnityEngine;
public class BindToInstanceSample : ContextView
{
    private void Awake()
    {
        var context = new Context(this);
        context.injectionBinder.Bind<IWeapon>().To<Knife>();
        context.injectionBinder.Bind<ISoldier>().To<Soldier>()
            .ToSingleton() // ToSingleton 表示单例绑定
            .ToName("Soldier"); // 给绑定关系起个名字
    }
}
public class Soldier : ISoldier
{
    [Inject]
    public IWeapon Weapon { get; set; }
    public void Attack()
    {
        Debug.Log($"Using {Weapon.Name} to attack!");
    }
}
public class Knife : IWeapon
{
    public string Name => "Knife";
}

在上面的代码中,我们将 IWeapon 接口绑定到 Knife 类型上,将 ISoldier 接口绑定到 Soldier 上,并设置了 Soldier 为 Singleton 单例,同时给它起了个名字 Soldier

3. 注入依赖

在上面的绑定代码中,使用了属性注入和构造函数注入两种方式来注入依赖。 在 PropsInjectionSample 中,我们使用属性注入方式向 Soldier 对象中注入 IWeapon 对象:

using strange.extensions.context.impl;
using UnityEngine;
public class PropsInjectionSample : ContextView
{
    [SerializeField]
    private Knife knife;
    private void Awake()
    {
        var context = new Context(this);
        context.injectionBinder.Bind<IWeapon>().To(() => knife).ToSingleton();
        context.injectionBinder.Bind<ISoldier>().To<Soldier>().ToSingleton();
    }
}

在上面的代码中,将 IWeapon 绑定到 Knife 类型上,并使用 To 方法,将注入对象作为一个函数传递,这样,每次调用 Inject 方法时,将会调用该函数来获得注入对象。 在 ConstructInjectionSample 类中,我们使用构造函数注入方式向 Soldier 对象中注入 IWeapon 对象:

using strange.extensions.context.impl;
using UnityEngine;
public class ConstructInjectionSample : ContextView
{
    [SerializeField]
    private Knife knife;
    private void Awake()
    {
        var context = new Context(this);
        context.injectionBinder.Bind<IWeapon>().To(() => knife).ToSingleton();
        context.injectionBinder.Bind<ISoldier>().To<SoldierWithConstruct>().ToSingleton();
    }
    private class SoldierWithConstruct : ISoldier
    {
        private readonly IWeapon _weapon;
        public SoldierWithConstruct(IWeapon weapon)
        {
            _weapon = weapon;
        }
        public void Attack()
        {
            Debug.Log($"Using {_weapon.Name} to attack!");
        }
    }
}

在上面的代码中,我们创建了一个新的 SoldierWithConstruct 类,由于 SoldierWithConstruct 存在一个 IWeapon 类型的参数,StrangeIoC 将会使用构造函数注入方式来创建这个对象,并将 IWeapon 对象自动注入进去。

StrangeIoC 的高级应用

在正式的应用程序中,你需要更加灵活的控制组件和依赖。下面简单介绍一下 StrangeIoC 的高级应用。

1. 模块化开发

由于绑定关系都是全局的,不同模块之间可能会有命名上的冲突。因此,我们可以使用子绑定器来实现模块化开发。

using strange.extensions.command.api;
using strange.extensions.command.impl;
using strange.extensions.context.api;
using strange.extensions.context.impl;
using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.dispatcher.eventdispatcher.impl;
using strange.extensions.injector.api;
using strange.extensions.injector.impl;
using UnityEngine;
public class ModuleSample : ContextView
{
    public static IInjectionBinder MainBinder;
    private void Awake()
    {
        var context = new Context(this, ContextStartupFlags.MANUAL_LAUNCH);
        context.injectionBinder.BinderName = "MainBinder";
        MainBinder = context.injectionBinder;
        context.Start();
        var subContext = new ModuleContext(this);
        subContext.Start();
    }
    public class CommonBundleInstaller : IInstaller
    {
        public void Install(IInjectionBinder binder)
        {
            binder.Bind<IFoo>().To<Foo>().ToSingleton();
        }
    }
    public class ModuleContext : MVCSContext
    {
        public ModuleContext(MonoBehaviour view) : base(view, ContextStartupFlags.MANUAL_LAUNCH)
        {
        }
        protected override void mapBindings()
        {
            injectionBinder.BinderName = "SubBinder";
            injectionBinder.Install(new CommonBundleInstaller());
            commandBinder.Bind<SomeEvent>().To<SomeEventCommand>();
        }
    }
    public interface IFoo
    {
    }
    public class Foo : IFoo
    {
    }
    public class SomeEventCommand : EventCommand
    {
        [Inject]
        public IFoo Foo { get; set; }
        public override void Execute()
        {
            Debug.Log("Do something with Foo instance.");
        }
    }
    public class SomeEvent : IEvent
    {
    }
}

在上面的例子中,我们定义了一个 CommonBundleInstaller 类,用于安装一些共享模块的绑定关系;然后我们定义了一个名为 SubBindersubContext,它是 MVCSContext 的一个派生类,它也有一个自己的绑定器。

2. 自定义命令绑定

在 StrangeIoC 中,命令绑定是用来处理用户事件的,它们绑定到事件上,并且当事件被触发时,命令将会被执行。你可以通过自定义命令来扩展 StrangeIoC 的功能。

using strange.extensions.command.impl;
using strange.extensions.context.api;
using strange.extensions.context.impl;
using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.dispatcher.eventdispatcher.impl;
using strange.extensions.injector.api;
using strange.extensions.injector.impl;
using UnityEngine;
public class CustomCommandBindingSample : ContextView
{
    private void Awake()
    {
        var context = new Context(this);
        context.commandBinder.Bind<SomeEvent>().To<MultiplyCommand>();
        context.dispatcher.Dispatch(new SomeEvent { IntValue = 10 });
    }
    public class SomeEvent : IEvent
    {
        public int IntValue { get; set; }
    }
    public class MultiplyCommand : Command
    {
        [Inject]
        public int Multiplier { get; set; } = 3;
        public override void Execute()
        {
            var e = evt.data as SomeEvent;
            Debug.Log(e.IntValue * Multiplier);
        }
    }
}

在上面的例子中,我们定义了一个自定义命令 MultiplyCommand,将 SomeEventMultiplyCommand 进行绑定。在命令类中,我们可以通过 [Inject] 标记来注入依赖项。由于依赖注入是自动完成的,我们可以在命令类中直接使用注入的对象来进行计算。

3. 自定义绑定提供者

StrangeIoC 默认支持的绑定关系是有限的,例如绑定一个类型到另一个类型需要指定 .To<T> 方法,但是如果你想通过其他的方式来定义绑定关系呢?在 StrangeIoC 中,你可以通过实现 IBindingProvider 接口来自定义绑定关系。

using System;
using strange.extensions.context.impl;
using UnityEngine;
public class CustomBindingProviderSample : ContextView
{
    private void Awake()
    {
        var context = new Context(this);
        context.injectionBinder.Unbind<int>();
        context.injectionBinder.Bind<int>().ToProvider(new RandomNumberProvider());
        var entity = new Entity(context.injectionBinder.GetInstance<int>());
        Debug.Log($"Entity instance ID={entity.InstanceId}, value={entity.Value}");
    }
    private class Entity
    {
        public int InstanceId { get; private set; }
        public int Value { get; private set; }
        public Entity(int value)
        {
            InstanceId = GetHashCode();
            Value = value;
        }
    }
    private class RandomNumberProvider : IBindingProvider
    {
        public Type GetBindingType()
        {
            return typeof(int);
        }
        public object GetBinding(IInjectionBinder binder)
        {
            return new System.Random().Next(100);
        }
    }
}