本文目的
向大家介绍:
- 在开发中为何要使用
IoC - 如何从0开始实现一个精简的
IoC - 使用
IoC 前后代码带来怎样的变化 - 我当前在开发的
IoC 类库
如果你对1 、2 、3 都已经很熟了,并且对我的项目感兴趣,可以直接跳我的IoC 仓库.完整的工程地址在https://github.com/kakashiio/Unity-IOC,该IoC 仓库也是我的Unity游戏框架计划https://github.com/kakashiio/Unity-SourceFramework中的一部分.
为什么要使用IoC
想象一下,当你在实现一个UI管理器UIManager 时,当在UIManager 中需要加载UI资源时,你是通过何种方式加载资源的.
一般开发诸如AssetManager 、TimeManager 、EventManager 等管理器(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 实例还好,实际上可以预见的是SceneManager 、UnitManager 、EffectManager 等类可能都需要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
{
object InstanceAndInject(Type type);
T InstanceAndInject<T>();
void Inject(object obj, bool recursive = false);
object FindObjectOfType(Type type);
T FindObjectOfType<T>() where T : class;
List<object> FindObjectsOfType(Type type);
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));
}
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容器需要的其他类定义
[AttributeUsage(AttributeTargets.Class)]
public class IOCComponent : Attribute
{
}
[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);
yield return new WaitForSeconds(3);
T loadedAsset = default(T);
_LogManager.Log(LogLevel.Debug, "Loaded {0} asset={1}", assetPath, loadedAsset);
onLoaded?.Invoke(loadedAsset);
}
}
资源管理类,可以看到该类依赖了CoroutineManager 和LogManager ,但是没有对外提供这两个对象的设置.
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管理类,可以看到该类也依赖了CoroutineManager 和LogManager ,和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;
var inheritedFromIOCComponent = Reflections.GetTypes(_TypeContainer, typeof(IOCComponent));
var typesWithIOCComponent = Reflections.GetTypesWithAttributes(_TypeContainer, inheritedFromIOCComponent);
foreach (var type in typesWithIOCComponent)
{
_Instances.Add(_Instance(type));
}
foreach (var instance in _Instances)
{
Inject(instance);
}
}
注释1 的代码表示从_TypeContainer 中获取从IOCComponent 这一Attribute 继承的所有Attribute ,如果_TypeContainer 中包含了IOCComponent ,那么返回的列表中也会有IOCComponent .
_TypeContainer 为ITypeContainer 类型,顾名思义,它是类型容器,用于返回我们可能需要处理的所有类型.具体使用我会在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)
{
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);
}
}
}
该方法主要用于对字段进行依赖注入.
注释1 主要用于当需要递归注入时,如果发现一个对象已经注入过,则跳过,防止递归陷入死循环.
注释2 获取obj 中所有使用Autowired 这一Attribute 修饰的字段或属性.Autowired 为前面定义的Attribute ,我们通过这一Attribute 标识哪些字段需要容器自动注入.
注释3 通过FindObjectOfType 从IoC 容器中找到类型和字段或属性类型相匹配的对象,查找会匹配类型.我们后面再细讲FindObjectOfType 是如何实现的.
注释4 将注释3 找到的对象设置进字段,完成该字段注入.
注释5 如果开启递归注入,则对该字段的值也进行注入.
FindObjectOfType方法
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;
}
注释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]
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);
yield return new WaitForSeconds(3);
T loadedAsset = default(T);
_LogManager.Log(LogLevel.Debug, "Loaded {0} asset={1}", assetPath, loadedAsset);
onLoaded?.Invoke(loadedAsset);
}
}
注释1 可以看到之前字段_LogManager 从LogManager 类型修改为接口类型ILogManager .
同样地,将GameObjectManager 中字段_LogManager 从LogManager 类型修改为接口类型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..." ,加入如下两个地址
致谢
感谢百忙之中阅读本文,如果觉得我的文章帮到了你,欢迎:转载、关注git、为仓库增加star等.你的简单回馈将是我继续创作的动力.
|