65.9K
CodeProject 正在变化。 阅读更多。
Home

Unity3d 的简单 IoC 容器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (19投票s)

2014年5月20日

CPOL

3分钟阅读

viewsIcon

29509

downloadIcon

892

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(效率最低的一种方法)之类的途径来获取它们。

它还非常清晰地显示了您的脚本依赖的内容。

© . All rights reserved.