当前位置: 首页 > 工具软件 > Go-IOC > 使用案例 >

Unity-IOC

马坚白
2023-12-01

本文目的

向大家介绍:

  1. 在开发中为何要使用IoC
  2. 如何从0开始实现一个精简的IoC
  3. 使用IoC前后代码带来怎样的变化
  4. 我当前在开发的IoC类库

如果你对123都已经很熟了,并且对我的项目感兴趣,可以直接跳我的IoC仓库.完整的工程地址在https://github.com/kakashiio/Unity-IOC,该IoC仓库也是我的Unity游戏框架计划https://github.com/kakashiio/Unity-SourceFramework中的一部分.

为什么要使用IoC

想象一下,当你在实现一个UI管理器UIManager时,当在UIManager中需要加载UI资源时,你是通过何种方式加载资源的.

一般开发诸如AssetManagerTimeManagerEventManager管理器(Manager)时.喜欢采用静态方法或单例.这样做是为了使得项目能方便地引用这些管理器.

常见的实现代码:


public class UIManager
{
    public void Create<T>(Action<T> onCreate) where T : IUI
    {
        string assetPath = _GetAssetPath<T>();
        AssetManager.Instantiate<GameObject>((go)=>{
            var t = new T();
            t.Init(go);
            onCreate?.Invoke(t);
        });
    }
}

public class UIManager
{
    public void Create<T>(Action<T> onCreate) where T : IUI
    {
        string assetPath = _GetAssetPath<T>();
        Singleton<AssetManager>.Instance.Instantiate<GameObject>((go)=>{
            var t = new T();
            t.Init(go);
            onCreate?.Invoke(t);
        });
    }
}

虽然静态方法或单例都能实现想要的效果,但或多或少会带来负面的效果.比如耦合严重,难以测试等等.因此本文引入一种已经很成熟的设计思路IoC,一步步实现一个简单的IoC容器,并且将IoC应用到实际中.大家也可以对比感受引入IoC前后代码发生的变化.

IoC简述

IoC(Inversion of Control,控制反转)通常也被称为DI(Dependency Injection,依赖注入).他是将传统对象依赖从内部指定改为外部决定的过程.比如上面的UIManager中内部指定了使用AssetManager.当使用IoC设计时,代码会修改为:


public class UIManager
{
    private IAssetManager _AssetManager;
    public UIManager(IAssetManager assetManager)
    {
        _AssetManager = assetManager;
    }

    public void Create<T>(Action<T> onCreate) where T : IUI
    {
        string assetPath = _GetAssetPath<T>();
        _AssetManager.Instantiate<GameObject>((go)=>{
            var t = new T();
            t.Init(go);
            onCreate?.Invoke(t);
        });
    }
}

这是引入IoC最简单的例子,即把内部采用哪个IAssetManager实现的权力转移给外部,因此称为IoC(Inversion of Control,控制反转),由于UIManager依赖了IAssetManager而且将其实现通过外部构造传入,因此也称DI(Dependency Injection,依赖注入).

但是这样的代码明显不够方便,因为需要自己在构造时传入IAssetManager,如果只是UIManager需要传入IAssetManager实例还好,实际上可以预见的是SceneManagerUnitManagerEffectManager等类可能都需要IAssetManager,那么最终可能会有类似这样的代码:

public class Main
{
    public void Init()
    {
        var assetManager = new AssetManager();
        var uiManager = new UIManager(assetManager);
        var sceneManager = new SceneManager(assetManager);
        var unitManager = new UnitManager(assetManager);
        var effectManager = new EffectManager(assetManager);
        // ...
    }
}

这样的代码重复、而且没有意义、不同的人反复在这里添加自己的代码也容易引发冲突和错误.我们应该编写一个更智能的IoC框架来帮助我们完成这些事情.

编写IoC框架

添加依赖

由于我们需要大量使用反射完成一些工作,因此通过PackageManager依赖我之前开源的用于反射的Packagehttps://github.com/kakashiio/Unity-Reflection

打开Unity的PackageManager并点击左上角的“+”按钮,选择"Add package from git URL..."并填入该地址https://github.com/kakashiio/Unity-Reflection.git#1.0.0

IoC容器

定义IoC容器接口

public interface IIOCContainer
{
    /// <summary>
    /// 实例化`type`类型的对象并注入其所有字段和属性
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    object InstanceAndInject(Type type);

    /// <summary>
    /// 实例化类型为`T`的对象并注入其所有字段和属性
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T InstanceAndInject<T>();

    /// <summary>
    /// 为一个已存在的对象`obj`注入其所有字段和属性
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="recursive">
    /// 如果recursive == true, 那么instance的字段也会被递归注入
    /// </param>
    void Inject(object obj, bool recursive = false);

    /// <summary>
    /// 查找`type`类型或`type`类型子类的对象.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    object FindObjectOfType(Type type);
    
    /// <summary>
    /// 查找`T`类型或`T`类型子类的对象.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T FindObjectOfType<T>() where T : class;
    
    /// <summary>
    /// 查找所有`type`类型或`type`类型子类的对象.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    List<object> FindObjectsOfType(Type type);
    
    /// <summary>
    /// 查找所有`T`类型或`T`类型子类的对象.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    List<T> FindObjectsOfType<T>() where T : class;
}

IIOCContainer接口主要定义了一个IOC容器对外提供的服务.比如外部可以通过FindObjectOfType查找某个类型在容器中创建的实例、或者通过InstanceAndInject创建一个指定类型的对象,InstanceAndInject方法与new创建对象不同在于InstanceAndInject创建的对象会被容器管理,同时会自动按设计的约定注入字段.

这里每个方法都写了比较详细的注释.如果目前大家还不是很清楚,主要可能是对于IoC还不太熟悉,这关系不大.后面会通过实际使用的例子回过来深入介绍细节.接下来先把该接口的实现和另外几个比较重要的类的源码给出来,目前大家只要先大概浏览一下即可.

实现IoC容器

public class IOCContainer : IIOCContainer
{
    private ITypeContainer _TypeContainer;
    private List<object> _Instances = new List<object>();
    private HashSet<object> _InjectedObj = new HashSet<object>();
    
    private Dictionary<Type, object> _FindCache = new Dictionary<Type, object>();

    public IOCContainer(ITypeContainer typeContainer)
    {
        _TypeContainer = typeContainer;

        var inheritedFromIOCComponent = Reflections.GetTypes(_TypeContainer, typeof(IOCComponent));
        var typesWithIOCComponent = Reflections.GetTypesWithAttributes(_TypeContainer, inheritedFromIOCComponent);

        foreach (var type in typesWithIOCComponent)
        {
            _Instances.Add(_Instance(type));
        }
        
        // Inject all type's field or property
        foreach (var instance in _Instances)
        {
            Inject(instance);
        }
    }

    public object InstanceAndInject(Type type)
    {
        var instance = _Instance(type);
        Inject(instance);
        return instance;
    }

    public T InstanceAndInject<T>()
    {
        return (T) InstanceAndInject(typeof(T));
    }

    public void Inject(object obj, bool recursive = false)
    {
        if (obj == null)
        {
            return;
        }

        if (obj.GetType().IsPrimitive)
        {
            return;
        }

        if (recursive)
        {
            if (_InjectedObj.Contains(obj))
            {
                return;
            }
            _InjectedObj.Add(obj);    
        }

        var propertiesOrFields = Reflections.GetPropertiesAndFields<Autowired>(obj);

        foreach (var propertyOrField in propertiesOrFields)
        {
            var fieldValue = FindObjectOfType(propertyOrField.GetFieldOrPropertyType());
            propertyOrField.SetValue(obj, fieldValue);
            
            if (recursive)
            {
                Inject(fieldValue, true);
            }
        }
    }

    public object FindObjectOfType(Type type)
    {
        if (_FindCache.ContainsKey(type))
        {
            return _FindCache[type];
        }

        foreach (object instance in _Instances)
        {
            if(type.IsAssignableFrom(instance.GetType()))
            {
                _FindCache.Add(type, instance);
                return instance;
            }
        } 
        return null;
    }

    public T FindObjectOfType<T>() where T : class
    {
        return FindObjectOfType(typeof(T)) as T;
    }

    public List<object> FindObjectsOfType(Type type)
    {
        return _FindObjectsOfType(typeof(object), o => o);
    }

    public List<T> FindObjectsOfType<T>() where T : class
    {
        return _FindObjectsOfType(typeof(T), o => o as T);
    }

    private object _Instance(Type type)
    {
        return Activator.CreateInstance(type);
    }
    
    private List<T> _FindObjectsOfType<T>(Type type, Func<object, T> mapper) where T : class
    {
        List<T> list = new List<T>();
        foreach (object instance in _Instances)
        {
            var objType = instance.GetType();
            if(type.IsAssignableFrom(objType))
            {
                list.Add(mapper(instance));
            }
        }
        return list;
    }
}

上面的实现中有几个类尚未定义,下面继续定义缺失的类.

IoC容器需要的其他类定义

/// IOC组件的Attribute
/// 当自定义的类上使用了该Attribute时,那么该类会被容器自动创建
[AttributeUsage(AttributeTargets.Class)]
public class IOCComponent : Attribute
{
}
/// 自动注入的Attribute
/// 标记了IOCComponent的类或通过IIOCContainer.InstanceAndInject、IIOCContainer.Inject
/// 创建的对象,其所有标记了Autowired的字段或属性会由IOC容器自动注入实例
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property)]
public class Autowired : Attribute
{
}

OK,依然如前所述,对于接触不多的人而言,该框架信息量确实比较大,请先放松.接下来通过实际使用的例子,再深入讲解上面的源码.

IoC框架使用示例

定义各种测试用Manager

日志管理类

[IOCComponent]
public class LogManager
{
    private LogLevel _LogLevel = LogLevel.Debug;
    
    public void Log(LogLevel level, string templte, params object[] args)
    {
        if (level < _LogLevel)
        {
            return;
        }
        string msg = args == null || args.Length == 0 ? templte : string.Format(templte, args);
        msg = $"[{level}] Frame={Time.frameCount} Time={Time.time} -- {msg}";
        switch (level)
        {
            case LogLevel.Debug:
            case LogLevel.Info:
                Debug.Log(msg);
                break;
            case LogLevel.Warning:
                Debug.LogWarning(msg);
                break;
            case LogLevel.Exception:
                Debug.LogException(new Exception(msg));
                break;
            case LogLevel.Error:
                Debug.LogError(msg);
                break;
        }
    }
}

public enum LogLevel
{
    Debug,
    Info,
    Warning,
    Exception,
    Error
}

该类只是用于做简单的日志记录,会被后续其他Manager依赖使用.

注意到这个管理类上使用IOCComponent这一Attribute进行修饰.后续其他管理类也是如此.后续会解释为什么要这么做.

协程管理类

[IOCComponent]
public class CoroutineManager
{
    private CoroutineRunner _CoroutineRunner;
    
    public CoroutineManager()
    {
        var go = new GameObject("CoroutineRunner");
        _CoroutineRunner = go.AddComponent<CoroutineRunner>();
        GameObject.DontDestroyOnLoad(go);
    }

    public void StartCoroutine(IEnumerator enumerator)
    {
        _CoroutineRunner.StartCoroutine(enumerator);
    }
}

public class CoroutineRunner : MonoBehaviour
{
}

该类只是用于简单的协程调用,会被后续其他Manager依赖使用

资源管理类

[IOCComponent]
public class AssetManager
{
    [Autowired] 
    private CoroutineManager _CoroutineManager;
    [Autowired] 
    private LogManager _LogManager;

    public void LoadAsync<T>(string assetPath, Action<T> onLoaded) where T : Object
    {
        _CoroutineManager.StartCoroutine(_LoadAsync(assetPath, onLoaded));
    }

    private IEnumerator _LoadAsync<T>(string assetPath, Action<T> onLoaded) where T : Object
    {
        _LogManager.Log(LogLevel.Debug, "Loading {0}", assetPath);
        // Your load code here
        // Now just wait for some seconds for demo
        yield return new WaitForSeconds(3);
        T loadedAsset = default(T);
        _LogManager.Log(LogLevel.Debug, "Loaded {0} asset={1}", assetPath, loadedAsset);
        onLoaded?.Invoke(loadedAsset);
    }
}

资源管理类,可以看到该类依赖了CoroutineManagerLogManager,但是没有对外提供这两个对象的设置.

GameObject管理类

[IOCComponent]
public class GameObjectManager
{
    [Autowired]
    private AssetManager _AssetManager;
    [Autowired]
    private LogManager _LogManager;

    public void Instantiate(string assetPath, Action<GameObject> onLoaded)
    {
        _AssetManager.LoadAsync(assetPath, (GameObject prefab) =>
        {
            if (prefab == null)
            {
                _LogManager.Log(LogLevel.Debug, "Failed to instantiate {0}", assetPath);
                return;
            }
            var go = GameObject.Instantiate(prefab);
            onLoaded?.Invoke(go);
        });
    }
}

GameObject管理类,可以看到该类也依赖了CoroutineManagerLogManager,和AssetManager一样没有对外提供这两个对象的设置.

那么,这样的代码是否能工作呢,我们接着编写测试类.

测试依赖注入

public class BasicDemo : MonoBehaviour
{
    private void Awake()
    {
        var typeContainer = new TypeContainerCollection(new []
        {
            new TypeContainer(Assembly.GetExecutingAssembly()),
            new TypeContainer(typeof(IOCComponent).Assembly)
        });
        var iocContainer = new IOCContainer(typeContainer);
        
        GameObjectManager gameObjectManager = iocContainer.FindObjectOfType<GameObjectManager>();
        gameObjectManager.Instantiate("", null);
    }
}

可以看到,这个类主要就是创建了一个IoC容器IOCContainer对象,接着从该容器中查找GameObjectManager,接着通过GameObjectManager实例化一个对象.

可以把该类挂到场景中任意对象上,然后运行场景.发现Unity会输出以下Log.

[20:46:43] [Debug] Frame=0 Time=0 -- Loading
[20:46:43] [Debug] Frame=643 Time=3.000951 -- Loaded asset=
[20:46:43] [Debug] Frame=643 Time=3.000951 -- Failed to instantiate

可以看到,我们并没有手动为各个Manager传入依赖,但是目前而言,通过IOCContainer为我们自动创建的Manager确实自动注入了依赖.

为何能实现注入

那么是什么时候创建了各个管理器的实例,又是什么时候设置了管理器之间的依赖.我们重新对IOCContainer的构造函数进行分析.

IOCContainer的构造函数

public IOCContainer(ITypeContainer typeContainer)
{
    _TypeContainer = typeContainer;
    
    /* 1 */
    var inheritedFromIOCComponent = Reflections.GetTypes(_TypeContainer, typeof(IOCComponent));
    /* 2 */
    var typesWithIOCComponent = Reflections.GetTypesWithAttributes(_TypeContainer, inheritedFromIOCComponent);

    /* 3 */
    foreach (var type in typesWithIOCComponent)
    {
        _Instances.Add(_Instance(type));
    }

    /* 4 */
    // Inject all type's field or property
    foreach (var instance in _Instances)
    {
        Inject(instance);
    }
}

注释1的代码表示从_TypeContainer中获取从IOCComponent这一Attribute继承的所有Attribute,如果_TypeContainer中包含了IOCComponent,那么返回的列表中也会有IOCComponent.

_TypeContainerITypeContainer类型,顾名思义,它是类型容器,用于返回我们可能需要处理的所有类型.具体使用我会在Unity-Reflection库中补充文档说明.

注释2的代码表示从_TypeContainer中获取类型列表,该列表中的类型需要满足:类上使用了inheritedFromIOCComponent列表中任意Attribute进行修饰.其实按我们目前的例子看,由于我们的所有Manager都使用了IOCComponent进行修饰,那么这里的列表如果仅包含IOCComponent,应当也能查询到我们定义的管理类.那么为什么不直接使用new List<Type> { typeof(IOCComponent) }替代注释1返回的inheritedFromIOCComponent呢.这是因为我想增加一点拓展性.当你想让自己定义的Attribute也能被IOCContainer识别时,你的Attribute可以从IOCComponent继承,那么注释1将能找到你自己定义的Attribute,此时你用自己定义的Attribute修饰类时,该类也能被查找到.

注释3的循环作用为遍历注释2返回的类型列表,并且调用_Instance方法将其实例化,并添加到_Instances列表中,以便后续有其他查找需求.目前_Instance方法只是简单通过Activator.CreateInstance(type);创建了实例并返回.

注释4的循环作用为遍历注释3实例化的_Instances列表,并调用Inject方法进行字段的依赖注入.我们的各个Manager字段的注入就是在此方法中进行的.接下来详细讲解Inject方法

Inject方法

public void Inject(object obj, bool recursive = false)
{
    if (obj == null)
    {
        return;
    }

    if (obj.GetType().IsPrimitive)
    {
        return;
    }

    if (recursive)
    {
        /* 1 */
        if (_InjectedObj.Contains(obj))
        {
            return;
        }
        _InjectedObj.Add(obj);    
    }

    /* 2 */
    var propertiesOrFields = Reflections.GetPropertiesAndFields<Autowired>(obj);

    foreach (var propertyOrField in propertiesOrFields)
    {
        /* 3 */
        var fieldValue = FindObjectOfType(propertyOrField.GetFieldOrPropertyType());
        /* 4 */
        propertyOrField.SetValue(obj, fieldValue);
        
        if (recursive)
        {
            /* 5 */
            Inject(fieldValue, true);
        }
    }
}

该方法主要用于对字段进行依赖注入.

注释1主要用于当需要递归注入时,如果发现一个对象已经注入过,则跳过,防止递归陷入死循环.

注释2获取obj中所有使用Autowired这一Attribute修饰的字段或属性.Autowired为前面定义的Attribute,我们通过这一Attribute标识哪些字段需要容器自动注入.

注释3通过FindObjectOfTypeIoC容器中找到类型和字段或属性类型相匹配的对象,查找会匹配类型.我们后面再细讲FindObjectOfType是如何实现的.

注释4注释3找到的对象设置进字段,完成该字段注入.

注释5如果开启递归注入,则对该字段的值也进行注入.

FindObjectOfType方法

public object FindObjectOfType(Type type)
{
    /* 1-Start */
    if (_FindCache.ContainsKey(type))
    {
        return _FindCache[type];
    }
    /* 1-End */

    foreach (object instance in _Instances)
    {
        /* 2-Start */
        if(type.IsAssignableFrom(instance.GetType()))
        {
            _FindCache.Add(type, instance);
            return instance;
        }
        /* 2-End */
    }    
    return null;
}

注释1-Start注释1-End中间的代码为从_FindCache中进行查找.如果之前已经通过该方法查到过该类型,那么该类型会进入_FindCache缓存,后续查找的时间复杂度就仅为O(1).

注释2-Start注释2-End中间的代码为从已经实例化的_Instances中查找有没有能赋值给type类型的对象,如果有,则加入到_FindCache缓存并且返回结果.可以发现这里使用了Type.IsAssignableFrom进行类型匹配,因此如果你的字段使用了接口或某个父类,也能正常进行注入.接下来我们增加一个ILogManager接口测试一下.

将LogManager改为接口

新增接口ILogManager

public interface ILogManager
{
    public void Log(LogLevel level, string templte, params object[] args);
}

public enum LogLevel
{
    Debug,
    Info,
    Warning,
    Exception,
    Error
}

新增ILogManager接口,并将LogManager中的枚举LogLevel删移动过来.

修改LogManager

[IOCComponent]
public class LogManager : ILogManager
{
    private LogLevel _LogLevel = LogLevel.Debug;
    
    public void Log(LogLevel level, string templte, params object[] args)
    {
        if (level < _LogLevel)
        {
            return;
        }
        string msg = args == null || args.Length == 0 ? templte : string.Format(templte, args);
        msg = $"[{level}] Frame={Time.frameCount} Time={Time.time} -- {msg}";
        switch (level)
        {
            case LogLevel.Debug:
            case LogLevel.Info:
                Debug.Log(msg);
                break;
            case LogLevel.Warning:
                Debug.LogWarning(msg);
                break;
            case LogLevel.Exception:
                Debug.LogException(new Exception(msg));
                break;
            case LogLevel.Error:
                Debug.LogError(msg);
                break;
        }
    }
}

LogManager实现ILogManager接口

修改AssetManager的字段

[IOCComponent]
public class AssetManager
{
    [Autowired] 
    private CoroutineManager _CoroutineManager;

    [Autowired] 
    /* 1 */
    private ILogManager _LogManager;

    public void LoadAsync<T>(string assetPath, Action<T> onLoaded) where T : Object
    {
        _CoroutineManager.StartCoroutine(_LoadAsync(assetPath, onLoaded));
    }

    private IEnumerator _LoadAsync<T>(string assetPath, Action<T> onLoaded) where T : Object
    {
        _LogManager.Log(LogLevel.Debug, "Loading {0}", assetPath);
        // Your load code here
        // Now just wait for some seconds for demo
        yield return new WaitForSeconds(3);
        T loadedAsset = default(T);
        _LogManager.Log(LogLevel.Debug, "Loaded {0} asset={1}", assetPath, loadedAsset);
        onLoaded?.Invoke(loadedAsset);
    }
}

注释1可以看到之前字段_LogManagerLogManager类型修改为接口类型ILogManager.

同样地,将GameObjectManager中字段_LogManagerLogManager类型修改为接口类型ILogManager.

重新运行场景,发现结果和之前不使用接口是一样的.

如果你想指定所有需要管理的类怎么实现

只需要去掉类定义上面的[IOCComponent],同时在构建IOCContainer时通过配置指定即可.

假设我们有如下的类

class You : IInstanceLifeCycle
{
    [Autowired]
    private Word _Word;
    
    [Autowired]
    [Qualifier(WORD_SPECIAL_INSTANCE)]
    private Word _Word2;

    public void Say()
    {
        Debug.LogError($"Say {_Word.GetMsg()}");
        Debug.LogError($"Say {_Word2.GetMsg()}");
    }

    public void BeforePropertiesOrFieldsSet()
    {
    }

    public void AfterPropertiesOrFieldsSet()
    {
    }

    public void AfterAllInstanceInit()
    {
        Say();
    }
}

class Word
{
    private string _Msg = "Hello";

    public string GetMsg()
    {
        return _Msg;
    }
}

可以看到You中依赖了两个Word类型的实例.有一个Word通过Qualifier指定了具体实例.

接下来看如何构造IOCContainer.

通过配置构造IOCContainer

public class SpecifyByHand : MonoBehaviour
{
    public const string WORD_SPECIAL_INSTANCE = nameof(WORD_SPECIAL_INSTANCE);
    void Start()
    {
        IOCContainerConfiguration config = new IOCContainerConfiguration()
            .AddConfigInstanceInfo<You>()
            .AddConfigInstanceInfo<Word>()
            .AddConfigInstanceInfo<Word>(WORD_SPECIAL_INSTANCE, new ValueSetter("_Msg", "Message"));
        new IOCContainerBuilder().SetConfiguration(config).Build();
    }
}

可以看到配置指定了创建两个Word实现和一个You,其中一个Word实例的Qualifier和上面You中字段上的Qualifier一致.

运行会输出:

Say Hello
Say Message

结束

以上为了更容易讲明白IoC的实现原理,一步步实现了一个极简的IoC容器,实际上该容器还缺少很多特性,比如AOP、比如支持通过配置指定注入不同实例等.更完整的IoC框架已经在下面GITHUB中开发维护.

完整的Package工程地址在https://github.com/kakashiio/Unity-IOC

使用

大家也可以通过PackageManager引用:打开Unity的PackageManager并点击左上角的“+”按钮,选择"Add package from git URL...",加入如下两个地址

Package地址说明Git地址
https://github.com/kakashiio/Unity-Reflection.git#1.0.0IOC依赖的反射库https://github.com/kakashiio/Unity-Reflection
https://github.com/kakashiio/Unity-IOC.git#1.0.0IOC容器库https://github.com/kakashiio/Unity-IOC

致谢

感谢百忙之中阅读本文,如果觉得我的文章帮到了你,欢迎:转载、关注git、为仓库增加star等.你的简单回馈将是我继续创作的动力.

 类似资料: