hadashiA / VContainer

The extra fast, minimum code size, GC-free DI (Dependency Injection) library running on Unity Game Engine.
https://vcontainer.hadashikick.jp
MIT License
2.02k stars 176 forks source link

How to Register Singletons that reference other Singletons? #702

Open fckaye opened 3 months ago

fckaye commented 3 months ago

I am quite new to this Dependency Injection thing in general, so I'm sorry if the question is silly or if there is a workaround that I am not aware of.

I was trying to follow the HelloWorld example on the documentation and start using VContainer from there.

Then I had a problem, when I was registering 2 classes which implement IStartable and one has a reference to another. I would get a message like the following:

VContainerException: Failed to resolve FooFeature.FooController : No such registration of type: BarFeature.BarController

I tried to reduce my app to a minimal setup so my trouble would be easier to understand/reproduce.

Imagine we have the following 5 scripts:

BarController

It basically takes BarView Button and OnClick, it will change the text contents of the Label of FooView through FooController

public class BarController : IStartable
    {
        private readonly BarView barView;
        private readonly FooController fooController;

        public BarController(BarView barView, FooController fooController)
        {
            this.barView = barView;
            this.fooController = fooController;
        }

        void IStartable.Start()
        {
            barView.BarButton.onClick.AddListener(() => fooController.UpdateLabelText("Bullshit coming from BarController"));
        }

        public void UpdateLabelText(string contents)
        {
            barView.BarLabel.text = $"BarLabel: \n{contents}";
        }
    }

BarView

    public class BarView : MonoBehaviour
    {
        public Button BarButton;
        public TextMeshProUGUI BarLabel;
    }

FooController

It basically takes FooView Button and OnClick, it will change the text contents of the Label of BarView through BarController

    public class FooController : IStartable
    {
        private readonly FooView fooView;
        private readonly BarController barController;

        public FooController(FooView fooView, BarController barController)
        {
            this.fooView = fooView;
            this.barController = barController;
        }

        void IStartable.Start()
        {
            fooView.FooButton.onClick.AddListener(() => barController.UpdateLabelText("Bullshit coming from FooController"));
        }

        public void UpdateLabelText(string contents)
        {
            fooView.FooLabel.text = $"FooLabel: \n{contents}";
        }
    }

FooView

    public class FooView : MonoBehaviour
    {
        public Button FooButton;
        public TextMeshProUGUI FooLabel;
    }

GameLifetimeScope

LifetimeScope to register components

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private FooView fooView;
    [SerializeField] private BarView barView; 

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponent(fooView);
        builder.RegisterComponent(barView);

        // If you have multiple Entry Points, you can also use the 
        // following declaration as grouping.
        builder.UseEntryPoints(Lifetime.Singleton, entryPoints =>
        {
            entryPoints.Add<FooController>();
            entryPoints.Add<BarController>();
        });
    }
}

Wiring up all the references on UnityEditor and pressing play will result in the following VContainerException:

VContainerException: Failed to resolve FooFeature.FooController : No such registration of type: BarFeature.BarController VContainer.Internal.ReflectionInjector.CreateInstance (VContainer.IObjectResolver resolver, System.Collections.Generic.IReadOnlyList`1[T] parameters) (at ./Library/PackageCache/jp.hadashikick.vcontainer@afaeff8204/Runtime/Internal/ReflectionInjector.cs:51) VContainer.Internal.InstanceProvider.SpawnInstance (VContainer.IObjectResolver resolver) (at ./Library/PackageCache/jp.hadashikick.vcontainer@afaeff8204/Runtime/Internal/InstanceProviders/InstanceProvider.cs:21)

I would really appreciate any help!

ZumiKua commented 2 months ago

The problem here is that you do not register BarController in your GameLifetimeScope, then VContainer does not know how to find a BarController for you. entryPoints.Add<BarController>(); only does two things:

  1. Make VContainer knows BarController is a IStartable (and other interfaces it implements), you can also do that by builder.Register<BarController>(Lifetime.Singleton).AsImplementedInterfaces(), now, everytime you require a IStartable, VContainer will throw you an instance of BarController.
  2. Make sure EntryPointDispatcher is registered, a class who is responsible for calling IStartable.Start()

Please note here, entryPoints.Add<BarController>(); does not tell VContainer what to do when someone needs a BarController, to tell VContainer that, you need the following code:

    entryPoints.Add<BarController>().AsSelf();

But here is another problem, your BarController requires FooController too, this creates a circular reference.

When VContainer create your FooController, it must create a BarController first, otherwise it has no argument for FooController's constructor. But when creating BarController, VContainer must find a FooController too. which creates a dead loop.

To solve this problem, you can remove the dependency of FooController from BarController's constructor, instead setting BarController.fooController at the end of FooController's constructor.

Please note, this is not the best practice, you should avoid circular dependency.

Here is all the code that changed:

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private FooView fooView;
    [SerializeField] private BarView barView; 

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponent(fooView);
        builder.RegisterComponent(barView);

        builder.UseEntryPoints(Lifetime.Singleton, entryPoints =>
        {
            entryPoints.Add<FooController>().AsSelf();
            entryPoints.Add<BarController>().AsSelf();
        });
    }
}
public class FooController : IStartable
{
    private readonly FooView fooView;
    private readonly BarController barController;

    public FooController(FooView fooView, BarController barController)
    {
        this.fooView = fooView;
        this.barController = barController;
        this.barController.fooController = this;
    }

    void IStartable.Start()
    {
        fooView.FooButton.onClick.AddListener(() => barController.UpdateLabelText("Bullshit coming from FooController"));
    }

    public void UpdateLabelText(string contents)
    {
        fooView.FooLabel.text = $"FooLabel: \n{contents}";
    }
}
public class BarController : IStartable
{
    private readonly BarView barView;
    public FooController fooController;

    public BarController(BarView barView)
    {
        this.barView = barView;
    }

    void IStartable.Start()
    {
        barView.BarButton.onClick.AddListener(() => fooController.UpdateLabelText("Bullshit coming from BarController"));
    }

    public void UpdateLabelText(string contents)
    {
        barView.BarLabel.text = $"BarLabel: \n{contents}";
    }
}