Unity3d 的简单 IoC 容器






4.96/5 (19投票s)
IoC 容器的实现,以便能够在 Unity3d 中使用依赖注入。
引言
在使用 Unity3d 一段时间后,我一直在寻找可用的 IoC 容器框架实现。我偶然发现了 Sebastiano Mandalà 在他的博客 这里 上的实现。虽然这是一个非常好的框架,但我并不完全满意,因为对于我的小型项目来说,该实现过于抽象,并且被拆分成了太多组件。此外,我来自微软阵营,经常在 WPF/ASP.NET 项目中使用 Prism.Unity 的 IoC 容器框架,因此习惯了它们的语法。
所以我开始为 Unity3d 实现我自己的 IoC 容器,它具有与Prism.Unity
类似的语法,基于 Sebastiano Mandalà 实现的一些想法。如果您喜欢更紧凑的解决方案和/或熟悉Prism.Unity
,那么这个框架可能是您开始使用的理想框架。
关于实现
接口
IoC 容器实现了一些接口,这些接口展示了 IoC 容器的不同任务
IIoCContainer
- 保存已注册类型的相关信息IServiceLocator
- 可以定位给定类型的已注册实现IDependencyInjector
- 可以将已知的依赖项注入到给定对象的字段或属性中
/// <summary>
/// Has the task to hold registrations of interfaces and implementations
/// </summary>
public interface IIoCContainer
{
/// <summary>
/// Adds the given type to the containers registry.
/// </summary>
/// <typeparam name="T">The type to register</typeparam>
void Register<T>() where T : class;
/// <summary>
/// Adds the given interface to the containers registry and links it with an implementation of this interface.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <typeparam name="TInterface">The type of the interface.</typeparam>
/// <typeparam name="TClass">The type of the class.</typeparam>
/// <param name="key">Optional.The key to
/// distinguish between implementations of the same interface.</param>
void Register<TInterface, TClass>(string key = null) where TClass : class, TInterface;
/// <summary>
/// Adds the given type to the containers registry. The instance of this type will be handled as a singleton.
/// </summary>
/// <typeparam name="T">The type to register</typeparam>
void RegisterSingleton<T>() where T : class;
/// <summary>
/// Adds the given instance to the containers registry. This instance will be handled as a singleton.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <typeparam name="T">The type to register</typeparam>
/// <param name="instance">
/// The instance that is going to be registered in the container.</param>
/// <param name="key">Optional.
/// The key to distinguish between implementations of the same interface.</param>
void RegisterSingleton<T>(T instance, string key = null) where T : class;
/// <summary>
/// Adds the given interface to the containers registry and links it with an implementation of this interface.
/// The instance of this type will be handled as a singleton.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <typeparam name="TInterface">The type of the interface.</typeparam>
/// <typeparam name="TClass">
/// The type of the class.</typeparam>
/// <param name="key">Optional.
/// The key to distinguish between implementations of the same interface.</param>
void RegisterSingleton<TInterface, TClass>(string key = null) where TClass : class, TInterface;
}
/// <summary>
/// Has the task to locate a registered instance of the given type.
/// </summary>
public interface IServiceLocator
{
/// <summary>
/// Resolves an instance of the given type.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <typeparam name="T">The type of the wanted object.</typeparam>
/// <param name="key">Optional.
/// The key to distinguish between implementations of the same interface.</param>
/// <returns>An instance of the given type.</returns>
T Resolve<T>(string key = null) where T : class;
/// <summary>
/// Resolves an instance of the given type.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <param name="type">The type of the wanted object.</param>
/// <param name="key">Optional.
/// The key to distinguish between implementations of the same interface.</param>
/// <returns>An instance of the given type.</returns>
object Resolve(Type type, string key = null);
}
/// <summary>
/// Has the task to provide dependencies of the given object.
/// </summary>
public interface IDependencyInjector
{
/// <summary>
/// Injects public properties or fields that are marked with the
/// Dependency attribute with the registered implementation.
/// </summary>
/// <param name="type">The type of the object.</param>
/// <param name="obj">The object whose dependencies should be injected.</param>
/// <returns>The injected object</returns>
object Inject(Type type, object obj);
/// <summary>
/// Injects public properties or fields that are marked with the
/// Dependency attribute with the registered implementation.
/// </summary>
/// <typeparam name="T">The type of the object</typeparam>
/// <param name="obj">The object whose dependencies should be injected.</param>
/// <returns>The injected object</returns>
T Inject<T>(object obj);
}
数据类
容器使用以下类来保存已注册类型的相关信息。该类有一个名为Create
的工厂方法,该方法收集并存储所有带有[Dependency]
属性的public
字段和属性。
private class TypeData
{
/// <summary>
/// Holds the instance of an already created singleton.
/// </summary>
public object Instance { get; set; }
/// <summary>
/// Holds all properties that need to be injected
/// </summary>
public List<KeyValuePair<DependencyAttribute, PropertyInfo>> Properties { get; private set; }
/// <summary>
/// Holds all fields that need to be injected
/// </summary>
public List<KeyValuePair<DependencyAttribute, FieldInfo>> Fields { get; private set; }
/// <summary>
/// Gets a value indicating whether this type should be handled as a singleton.
/// </summary>
public bool IsSingleton { get; private set; }
private TypeData()
{
this.Properties = new List<KeyValuePair<DependencyAttribute, PropertyInfo>>();
this.Fields = new List<KeyValuePair<DependencyAttribute, FieldInfo>>();
}
/// <summary>
/// Creates a new TypeData an gets all information needed to instantiate the given type.
/// </summary>
public static TypeData Create(Type type, bool isSingleton = false, object instance = null)
{
var typeData = new TypeData { IsSingleton = isSingleton, Instance = instance };
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
var dependency =
(DependencyAttribute)field.GetCustomAttributes(typeof(DependencyAttribute), true).FirstOrDefault();
if (dependency == null) continue;
typeData.Fields.Add(new KeyValuePair<DependencyAttribute, FieldInfo>(dependency, field));
}
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var dependency =
(DependencyAttribute)property.GetCustomAttributes(typeof(DependencyAttribute), true).FirstOrDefault();
if (dependency == null) continue;
typeData.Properties.Add(new KeyValuePair<DependencyAttribute, PropertyInfo>(dependency, property));
}
return typeData;
}
}
容器
容器需要两个字典。一个用于将接口连接到它们的实现,另一个用于将实现与其类型数据连接。
/// <summary>
/// Holds all information of the registered types.
/// </summary>
private readonly Dictionary<Type, Dictionary<string, Type>>
types = new Dictionary<Type, Dictionary<string, Type>>();
/// <summary>
/// The type containers
/// </summary>
private readonly Dictionary<Type, TypeData> typeDatas = new Dictionary<Type, TypeData>();
这些字典在private Register
方法中用于存储类型信息。
private void Register(Type interfaceType, Type type, TypeData typeData, string key = null)
{
try
{
if (this.types.ContainsKey(interfaceType ?? type))
{
this.types[interfaceType ?? type].Add(key ?? string.Empty, type);
}
else
{
this.types.Add(interfaceType ?? type, new Dictionary<string, Type> { { key ?? string.Empty, type } });
}
this.typeDatas.Add(type, typeData);
}
catch (Exception ex)
{
throw new IoCContainerException("Register type failed.", ex);
}
}
注册类之后,可以解析这些类型。如果要解析的类型派生自MonoBehavior
,则通过在GameObject
上调用AddComponent
来创建脚本。
/// <summary>
/// Resolves an instance of the given type.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <param name="type">The type of the wanted object.</param>
/// <param name="key">Optional.
/// The key to distinguish between implementations of the same interface.</param>
/// <returns>
/// An instance of the given type.
/// </returns>
public object Resolve(Type type, string key = null)
{
Guard(!this.types.ContainsKey(type), "The type {0} is not registered.", type.Name);
Guard(!this.types[type].ContainsKey(key ?? string.Empty),
"There is no implementation registered with the key {0} for the type {1}.", key, type.Name);
var foundType = this.types[type][key ?? string.Empty];
var typeData = this.typeDatas[foundType];
if (foundType.IsSubclassOf(typeof(MonoBehaviour))) // this is the unity3d specific part
{
Guard(this.singletonGameObjectName == null,
"You have to set a game object name to use for MonoBehaviours with SetSingletonGameObject() first.");
// places a new empty game object in the game if not found
var gameObject = GameObject.Find(this.singletonGameObjectName)
?? new GameObject(this.singletonGameObjectName);
// when the game already has the wanted component attached, return it.
// if that is not the case add the component to the object and inject possible dependencies.
return gameObject.GetComponent(type.Name) ?? Inject(foundType, gameObject.AddComponent(foundType));
}
if (typeData.IsSingleton)
{
// if an instance already exists, return it.
// if that is not the case setup a new instance and inject all dependencies.
return typeData.Instance ?? (typeData.Instance = this.Setup(foundType));
}
return this.Setup(foundType);
}
Using the Code
IoC 容器设置
要设置 IoC 容器,需要执行几个步骤。为了举例说明,我将向您展示如何在 Unity3d 项目中进行设置。
首先,创建一个新的 Unity3d 项目并将Framework文件夹复制到Assets文件夹中
接下来,您必须实现AbstractBootstrapper.cs。此类将成为 IoC 容器的入口点。您必须重写Configure
方法,在该方法中放置您要注册的所有服务/组件。
public class Bootstrapper : AbstractBootstrapper
{
public override void Configure(IIoCContainer container)
{
// non singleton
container.Register<IColorItem, ColorItem>();
// singletons
// multiple implementations
container.RegisterSingleton<IColorFactory, RedColorFactory>("red");
container.RegisterSingleton<IColorFactory, GreenColorFactory>("green");
container.RegisterSingleton<IColorFactory, BlueColorFactory>("blue");
// monobehaviour
container.RegisterSingleton<IColorHistory, ColorHistory>();
}
}
接下来,创建一个空的GameObject
并为其命名(例如,Container
),然后将 Bootstrapper 添加为组件
现在,我们需要强制 Unity 在应用程序启动时将Bootstrapper
作为第一个MonoBehaviour
执行。操作方法如下:
左键单击项目中的**任何**脚本以在检查器中显示以下视图,然后单击“执行顺序...”
检查器视图将更改。单击加号图标并选择Bootstrapper
。完成。
将依赖项注入 MonoBehaviour 脚本
如果要注入依赖项,只需使用[Dependency]
属性标记相应的public
属性或字段,并在Start
中调用Inject
方法即可。Inject
方法是 IoC 容器实现附带的扩展方法。
public class ColorDropper : MonoBehaviour
{
[Dependency("red")] public IColorFactory RedColorFactory;
[Dependency("blue")] public IColorFactory BlueColorFactory;
[Dependency("green")] public IColorFactory GreenColorFactory;
void Start () {
this.Inject();
this.StartCoroutine(this.DropColor());
}
关注点
示例项目
您可以下载的示例项目仅展示了如何以不同方式使用容器。示例本身的实现方式可能与实际操作方式不同。
我的观点
我最喜欢在 Unity 中使用 IoC 容器的一点是,它为您提供了一个更清晰的指导,说明如何获取其他GameObjects
的组件。您无需考虑使用诸如GameObject.Find
(效率最低的一种方法)之类的途径来获取它们。
它还非常清晰地显示了您的脚本依赖的内容。