simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 153 forks source link

Method Injection using Unity3D #871

Closed canuszczykpace closed 3 years ago

canuszczykpace commented 3 years ago

I'm trying to use Simple Injector with Unity3D. Their Monobehaviour classes are created by the system. Is there a way to make a fake "constructor" so that I can inject into it at Awake/Start time?

dotnetjunkie commented 3 years ago

Can you provide me with more information about your use case? How do you want to inject your dependencies, what does it mean t o have a "fake" constructor, and what is "Awake/Start time?"

canuszczykpace commented 3 years ago

Sure: In Unity 3D there is a class called a Monobehaviour that I have no control over when it gets instantiated. There are two methods that can be overridden - Awake() and Start() that are always called if provided.

Currently there is an IOC container that I have been using called ADIC that has been written and designed specifically for Unity but it isn't being updated any longer and is quite buggy. The way it works is by providing an extension method called .Inject() that attaches to MonoBehaviour classes. We decorate a method with an attribute called [Inject] and the .Inject extension method injects the parameters of the method in accordance to bindings to a container.

Pretty straightforward and easy to implement.

So the method is what I was calling a "Fake" constructor. Really just a method that will be present anytime I want things injected similar to a constructor.

Hope that makes sense.

dotnetjunkie commented 3 years ago

Although method injection is not supported OOTB, there is a code sample that demonstrates method injection. If you use that sample code, you can enable method injection by making the following call at startup:

// InjectAttribute is your [Inject] attribute
container.Options.EnableMethodInjectionWith<InjectAttribute>();

After that, make sure you register all MonoBehaviour implementations in the container. With Simple Injector's default behavior this is required to get the following Inject method to work. But besides, registering those MonoBehaviours is advised because, it allows them to be part of the verification process. Your injection methods will take part of this verification process. Here's how to register those behaviours:

foreach (Type type in container.GetTypesToRegister<MonoBehavior>(someAssemblies))
{
    container.Register(type);
}

That leaves you with the implementation of the .Inject() extension method, which would be the following:

public static class InjectExtensions
{
    // Don't forget to initialize this property at startup.
    public static SimpleInjector.Container Container { get; set; }

    public static void Inject(this MonoBehaviour behaviour)
    {
        if (behavior is null) throw new ArgumentNullException(nameof(behaviour));
        if (Container is null) throw new InvalidOperationException("Container not set.");

        var prod = container.GetRegistration(behavior.GetType(), throwOnFailure: true);
        prod.Registration.InitializeInstance(behaviour);
    }
}

The 'trick' applied in this Inject extension method is the call to Registration.InitializeInstance. This method exists apply initialization to an externally created instance. In most cases there is no initialization, because most classes will use constructor injection as their sole method of composition. Simple Injector, however, can be extended to do all kinds of post-constructor initialization, such as property injection or the above method injection. When calling InitializeInstance the existing instance is fed to the initialization pipeline.

Warnings ahead

Be aware, however, that the use of method injection comes with the same downsides as Property Injection, which basically means: Temporal Coupling. And in the case of the Inject attribute, it gets worse because you have to resort to the Service Locator anti-pattern.

But there might be little choice here, because Unity applies the Constraint Construction anti-pattern. When frameworks apply anti-patterns, there's often little left than stacking anti-patterns on top of them.

Humble Objects

Let me start by saying that I have no experience in Unity3D whatsoever, so the following idea might be completely besides the point and unusable. However, an often applied technique when dealing with frameworks that apply Constraint Construction, is to make the constraint objects Humble Objects. You can consider Humble Objects to be part of the Composition Root. The idea is than to extract all logic and dependencies out of the Humble Object into a new component. That component will solely use Constructor Injection as its injection pattern. From within the Humble Object you request that new component from the Container and invoke its (sole) public method.

This method, however, makes a few assumptions:

  1. MonoBehaviour classes can be made part of the Composition Root (which basically means placed in the application's startup project)
  2. It is possible to extract most (if not all) behavior out of MonoBehaviour classes into another components. (tip: you can supply the MonoBehaviour instance to the invoked method of the component in order to let the new component have access to the behaviour's data).

Whether or not this is possible and leads to better a better solution, is something I can't decide, but perhaps this idea is helpful.

canuszczykpace commented 3 years ago

Wow - what a great answer. Thanks a million.

Humble Objects - I wish. :)

Unfortunately, Monobehaviours are attached to GameObjects which are drag and drop objects in the "scene"'s hierarchy. FWIW - This helps form a link between developers and technical artists as public properties in the monobehaviours are exposed in the IDE's property inspector. It's not too bad except for creating this anti-pattern as you say. With a little finesse I hope to steer my team away from creating the monolithic classes that the monobehaviours tend to encourage.