Dependency Provider

NOTE: I no longer use, or recommend, the DependencyProvider. There are a number of reasons why this is the case, not the least of which because it didn’t work on certain platforms. But by not using it, I found myself missing its ability to cache found references to speed up lookup time, and didn’t need the object instantiation and lifetime management stuff so much. So I kept what I liked, ditched what I didn’t, and ended up with the much simpler DependencyLocator I now use in lieu of FindObjectOfType.

// --------------------------------
// <copyright file="DependencyLocator.cs" company="Rumor Games">
//     Copyright (C) Rumor Games, LLC.  All rights reserved.
// </copyright>
// --------------------------------

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// DependencyLocator class.
/// </summary>
public class DependencyLocator : MonoBehaviour
{
    /// <summary>
    /// Cached DependencyLocator instance.
    /// </summary>
    private static DependencyLocator dependencyLocator;

    /// <summary>
    /// Cache references to component instances.
    /// </summary>
    private readonly IDictionary<Type, Component> instances = new Dictionary<Type, Component>();

    /// <summary>
    /// Locate an instance of the given component type.
    /// </summary>
    /// <typeparam name="T">Type of component to locate.</typeparam>
    /// <returns>Instance of the desired component.</returns>
    public static T Find<T>() where T : Component
    {
        // check whether we have a dependency locator instance
        if (dependencyLocator == null)
        {
            // create a new game object and attach our component
            var gameObject = new GameObject(typeof(DependencyLocator).ToString());
            dependencyLocator = gameObject.AddComponent<DependencyLocator>(); 
        }

        // ask scene dependency locator for a scene instance
        return dependencyLocator.Get<T>();
    }

    /// <summary>
    /// Provide an instance of the given component type by retrieving it from the cache or finding one in the scene.
    /// </summary>
    /// <typeparam name="T">Type of component to provide.</typeparam>
    /// <returns>Instance of the desired component.</returns>
    public T Get<T>() where T : Component
    {
        var type = typeof(T);
        if (!this.instances.ContainsKey(type))
        {
            this.instances.Add(type, FindObjectOfType<T>());
        }

        return (T)this.instances[type];
    }
}

Original post:

Previously, I wrote about the process of locating dependencies in Unity. I enumerated some of the reasons the singleton pattern leads to bad practice, and how good process lead to better, more maintainable code.

And fine, I’ll admit it. I think singletons are ugly and I don’t like them:

TypicalSingleton.Instance.DoSomething(); // gross

Instead, I’d like to offer an alternative, one that doesn’t require a bunch of boilerplate code in every class I want to make globally and performantly accessible. The idea is based on the service locator pattern. While something of an anti-pattern in traditional software development, in Unity, where most of the pattern’s drawbacks come with the territory, it serves a valuable role.

I call my implementation the DependencyProvider. Where singletons require the dependency to change to support the pattern, the DependencyProvider works automatically with any component. It combines the flexibility of FindObjectOfType with the global access and performance of a singleton. It separates the responsibility for acquiring a reference to a dependency from the behavior of that dependency. Unlike a Singleton instance getter, it’s harder to forget that DependencyProvider is doing work under the hood to serve me that dependency, and easier to remember to use it responsibly and to stick to good programming practice.

internal void Start()
{
    if (this.typicalDependency == null)
    {
        this.typicalDependency = DependencyProvider.Get<TypicalDependency>();
    }
}

That said, the DependencyProvider class itself is NOT an example of good programming practice!

// --------------------------------
// <copyright file="DependencyProvider.cs" company="Rumor Games">
//     Copyright (C) Rumor Games, LLC.  All rights reserved.
// </copyright>
// --------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;

/// <summary>
/// Provides dependencies on request.
/// </summary>
public class DependencyProvider
{
    /// <summary>
    /// Cache references to global instances.
    /// </summary>
    private static readonly IDictionary<Type, Component> GlobalInstances = new Dictionary<Type, Component>();

    /// <summary>
    /// Delegate responsibility for caching scene instances to a SceneDependencyProvider.
    /// </summary>
    private static SceneDependencyProvider sceneDependencyProvider;

    /// <summary>
    /// Provide an instance of the given component type by retrieving it from the cache, finding one in the scene, or creating a new instance.
    /// </summary>
    /// <typeparam name="T">Type of component to provide.</typeparam>
    /// <returns>Instance of the desired component.</returns>
    public static T Get<T>() where T : Component
    {
        var type = typeof(T);

        // check whether the dependency we want is a global
        if (type.IsDefined(typeof(GlobalDependencyAttribute), false))
        {
            // check whether the global instance exists
            if (!GlobalInstances.ContainsKey(type))
            {
                GlobalInstances[type] = FindOrCreate<T>(true);
            }

            return (T)GlobalInstances[type];
        }

        // check whether we have a scene dependency provider
        if (sceneDependencyProvider == null)
        {
            sceneDependencyProvider = FindOrCreate<SceneDependencyProvider>(false);
        }

        // ask scene dependency provider for any scene instances
        return sceneDependencyProvider.FindOrCreate<T>();
    }

    /// <summary>
    /// Consumers: Use DependencyProvider.Get to ensure proper caching of instances.
    /// Provide an instance of the given component type by finding one in the scene or creating a new instance.
    /// </summary>
    /// <typeparam name="T">Type of component to provide.</typeparam>
    /// <param name="isGlobal">If true, don't destroy on load, creating a global-lifetime instance.</param>
    /// <returns>Instance of the desired component, either scene-lifetime or global-lifetime.</returns>
    protected static T FindOrCreate<T>(bool isGlobal) where T : Component
    {
        // check whether an instance already exists in the scene
        var instance = UnityEngine.Object.FindObjectOfType<T>();
        if (instance == null)
        {
            // create a new game object and attach our component
            var gameObject = new GameObject(string.Format(CultureInfo.CurrentCulture, "({0}) {1}", isGlobal ? "global" : "scene", typeof(T)));
            instance = gameObject.AddComponent<T>();
        }

        return instance;
    }

    /// <summary>
    /// Provides scene (not global) dependencies.
    /// </summary>
    protected class SceneDependencyProvider : MonoBehaviour
    {
        /// <summary>
        /// Cache references to scene instances.
        /// </summary>
        private readonly IDictionary<Type, Component> instances = new Dictionary<Type, Component>();

        /// <summary>
        /// Consumers: Use DependencyProvider.Get to ensure proper caching of instances.
        /// Provide an instance of the given component type by retrieving it from the cache, finding one in the scene, or creating a new instance.
        /// </summary>
        /// <typeparam name="T">Type of component to provide.</typeparam>
        /// <returns>Scene-lifetime instance of the desired component.</returns>
        public T FindOrCreate<T>() where T : Component
        {
            var type = typeof(T);
            if (!instances.ContainsKey(type))
            {
                instances.Add(type, DependencyProvider.FindOrCreate<T>(false));
            }

            return (T)instances[type];
        }
    }
}

Okay, that’s not entirely fair. This code works remarkably well within the Unity environment, but it only does so by exploiting how Unity itself works. Before I get to that, let’s go over what the DependencyProvider does and how it does it:

Just like with our example singleton, the core idea is to 1) return an existing instance if we have one, or 2) find one in the scene, or 3) construct a new one. The key difference is that, instead of each class implementing this functionality independently (or deriving from a base class with this functionality), one class takes care of everything.

The DependencyProvider separates dependencies into two types: global dependencies are types that are supposed to stick around for the entire runtime of the application (any types that you DontDestroyOnLoad), while scene dependencies are discarded when a scene is torn down. To simplify lifecycle management, the static DependencyProvider tracks only global dependencies and delegates scene dependencies to the SceneDependencyProvider. The SceneDependencyProvider is a regular MonoBehaviour and is discarded between scenes, effectively removing the scene dependencies from the cache. Global dependencies must handle their own lifecycle management, namely calling DontDestroyOnLoad in Awake or Start. There is one additional requirement – in order for DependencyProvider to know which types are global and which are scene-specific, we need to add a lightweight markup attribute to global classes:

// --------------------------------
// <copyright file="GlobalDependencyAttribute.cs" company="Rumor Games">
//     Copyright (C) Rumor Games, LLC.  All rights reserved.
// </copyright>
// --------------------------------

using System;

/// <summary>
/// GlobalDependencyAttribute class.
/// </summary>
public class GlobalDependencyAttribute : Attribute
{
}

Now, why is the DependencyProvider bad programming practice? Well, by all rights, it shouldn’t work. FindOrCreate is designated as protected, since it’s an implementation detail that clients shouldn’t have access to. The nested class SceneDependencyProvider calls it anyway, and it apparently works. Additionally, Unity instantiates SceneDependencyProvider just fine, despite being a nested, protected class. I’m guessing this works for the same reason it doesn’t matter whether MonoBehaviour.Update is marked private or public: Unity likely uses reflection to wire up to the pieces of a script it needs to interact with.

(Coincidentally, if you were wondering why I mark all my MonoBehaviour members internal, you can blame Unity’s selective disregard for access levels. Since my Unity projects generally don’t cross assemblies, internal is effectively the same as public, but using an uncommon keyword helps me tell at a glance whether the method I’m looking at is a MonoBehaviour member or my own method. It’s a simple trick that helps me keep my code organized and readable.)

Meanwhile, the sun may be setting on this particular trick. For whatever reason, it doesn’t work on the Windows Phone platform, probably because the .NET implementation isn’t as fast and loose with the language rules as the Mono version. (If anyone has more insight into why this is, please feel free to share in the comments.) But for the time being, it’s a useful “clever” trick that accomplishes exactly what I want it to.

And as one final note, the DependencyProvider of course comes with some of the same warnings as singletons: Don’t call DependencyProvider.Get everywhere in lieu of keeping a local reference to the dependency, call it once somewhere appropriate (like in Start) and cache the reference. Don’t call DependencyProvider.Get in OnDestroy, you could end up creating new instances of destroyed objects, resulting in leftover scene objects. Remember, good process is just as import as good tools!

Bookmark the permalink.

Comments are closed