Unity Dependency Locator

Posted On: 2020-11-30

By Mark

While working in Unity, I often have components depend upon other components. This is fairly unremarkable, as that is (generally) how Unity is intended to be used. Unfortunately, this also has cost me time, as I've had to troubleshoot simple mistakes, such as forgetting to attach the required dependency to the corresponding field. While I could try to detect such mistakes using automated content validation, I've instead opted to (partially) automate field assignment, by leveraging the service locator pattern. By doing so, I've cut back on the number of mistakes, as well as accellerated resolving the few that remain.

What is a Service Locator?

The service locator pattern is a programming pattern for managing dependencies that relies on calling an external source (the "locator") to provide an instance of the dependency (the "service".) This provides a level of indirection so that the code that requires a dependency doesn't need to know how to get that dependency - which can be useful when that needs to be complex and/or reconfigurable*. While convenient, the service locator does not completely isolate a developer from mistakes: the code must still call the locator, and the locator itself may require configuration before it can be used**.

Using a service locator to provide components is nothing new - Unity itself has a number of built-in service locators* that developers can (and often do) use. In light of this, I chose to design my implementation as a wrapper around a select few built-in service locators. When paired with consistent conventions and error handling, this wrapper becomes much more usable than relying on the built-in ones directly.

Implementation

Sticking to a convention can radically simplify service locator design as well as make for a more consistent user experience. For my implementation, I chose the following set of conventions*:

  1. Any dependency set in the inspector has priority over any other source
  2. Components on the same GameObject are considered next, after the inspector value
  3. Components on child GameObjects are considered last, and only when the caller specifies to do so
  4. If the dependency is not found, it should log an error and disable the current component

Additionally, as an implementation detail, I chose to use an extension method, that leverages generics to streamline the call as much as possible. Thus, any component can simply call dependency = this.Requires(dependency); in order to locate the dependency*. Lastly, callers can have confidence that, so long as the component is enabled, dependencies will be non-null.

For those that are curious, the code for the helper class is provided (collapsed) below:

//Extension methods are static methods that can be used as though they are part of some other class
//  It's really just syntactic sugar, but it makes the code more readable
public static class MonoBehaviourExtensions
{

    /// <summary>
    /// Search for a required dependency and return it. If missing, an error will be logged and the MonoBehaviour will be disabled.
    /// </summary>
    /// <typeparam name="T">Type of dependency</typeparam>
    /// <param name="monoBehaviour">The monobehavior that this extension is added to.</param>
    /// <param name="reference">An existing instance of the dependency. If non-null, this object will be returned.</param>
    /// <param name="searchChildren">Whether children should also be searched for the dependency.</param>
    /// <returns>An instance of the type specified. May be null if none found.</returns>
    public static T Require<T>(this MonoBehaviour monoBehaviour, T reference = default(T), bool searchChildren = false)
    {
        //If the reference is provided, return it immediately
        if (reference != null && !reference.Equals(default(T)))
            return reference;

        //Search this object for the component
        var element = monoBehaviour.GetComponent<T>();

        //Optionally, search children for component
        if ((element == null || element.Equals(default(T))) && searchChildren)
        {
            element = monoBehaviour.GetComponentInChildren<T>();
        }

        //Component not found, Log error and disable monobehavior
        if (element == null || element.Equals(default(T)))
        {
            Debug.LogError(monoBehaviour.name + " requires a component of type " + typeof(T) + " but none found.");
            monoBehaviour.enabled = false;
        }

        return element;
    }

Since first implementing this service locator*, I've greatly benefited from the convenience and clear error messaging it provides. Simple, easily fixed mistakes are handled by the system directly, whereas more significant issues (such as missing the dependency entirely) is clearly reported and the offending object disabled (so it doesn't break the rest of the program.) This has helped me countless times, by both enabling me to move forward with any manual testing I'd planned**, while also clearly documenting any dependency issues so that they can be resolved after manual testing completes. Furthermore, since the service locator is self-contained, it is simple to transfer between projects - making it an essential part of nearly every Unity project I make.

Conclusion

I hope this post about how I'm leveraging a convention-based service locator to reduce human error in my project has been interesting. While I have no expectation that my code will provide the right conventions for every team, I hope that, through reading my approach and design goals, you can see the value of using a single, standardized locator in a project.