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
1.9k stars 164 forks source link

Unable to register a component in the ParentScope if that component has dependencies from a ChildScope. #509

Closed SimonNordon4 closed 1 year ago

SimonNordon4 commented 1 year ago

For example I have a child scope called CharacterScope, which registers data like CharacterMovement, CharacterStats etc.

I also have a monobehaviour attached to the CharacterScope called CharacterFacade. The Character Facade implements many interfaces like ITarget, IDamageable, IPosition, IRotation etc.

The Game Scope is the parent scope, I want to inject the PlayerFacade as an IPosition into the Camera Controller. The PlayerFacade itself is injected with every dependency in the PlayerScope.

This throws a null reference, because when the parent scope attempt to register the facade, it find that all of it's values are null as the child container has not been built yet, and the instance has not been injected.

register.BuildCallback does not solve the issue, as it's only called after the current container has been built, not including all of it's children.

I can register the facade as a factory, but then I also need to register IPosition as Func. Now every object is being injected with Func instead of IPosition. Expand this for the other interfaces and it becomes very messy where everything is wrapped in a Func<>

Here is some code.


ChildScope

    public class ChildScope : GameObjectScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            builder.Register<TestData>(Lifetime.Scoped).WithParameter(1).WithParameter("Hello World!");
        }
    }

ChildFacade

    public class ChildFacade : MonoBehaviour
    {
        [SerializeReference]
        [Inject]private TestData testData;

        public TestData TestData => testData;
    }

ParentScope

    public class ParentScope : GameObjectScope
    {
        [SerializeField]private ChildFacade childFacade;

        protected override void Configure(IContainerBuilder builder)
        {
            // THROWS NULL!
            // builder.RegisterInstance(childFacade);

            // THROWS NULL!
            // builder.RegisterBuildCallback(c =>
            // {
            //     builder.RegisterInstance(childFacade);
            // });

            // DOES NOT THROW NULL.
            builder.RegisterFactory<ChildFacade>(() => childFacade);
        }
    }

I tried to do the same thing in Zenject and it works with the exact same code (Using Bind<> instead of register)

I really tried to use VContainer but I think I have to use Zenject, I already wrote a lot of custom code to accommodate it and I'm just moving too slowly. Taught me a lot about IoC though and I think I can use Zenject much more carefully now.

hadashiA commented 1 year ago

The fact that CharacterFacade is a unit of "scope" for DI is very puzzling to me. Why is it necessary to do so?

DI's scope is not designed to manage a large number of dynamically appearing objects such as Character.

And the Character data held by a single Character is not a "dependent object" in DI's view. It is just data. If you DI the "function" of holding data, that's understandable. But to DI "data" is an abuse of DI.

To me, this use case is puzzling.

However, it can be said that it is merely a difference in concept from Zenject.

SimonNordon4 commented 1 year ago

Just for reference, there's only one instance of Character in the game.

My goal was to break up scope into smaller chunks that are easier to maintain. Otherwise everything will have to be registered in the same LifetimeScope.

So to avoid a large number of classes affecting the characters data, I like to use a facade.

Your point is valid though, it's a different way of looking at the issue.

hadashiA commented 1 year ago

Otherwise everything will have to be registered in the same LifetimeScope.

For me, I don't really agree.

I think you are confusing the function of encapsulating data with the function of providing data.

There is no need for multiple in-memory DBs that access data based on certain keys. One is sufficient.

So the difference is whether you implement the data provider in the application or let the DI container do all the work.

My position is on the former. I don't think it's a good idea to have DI handle all the data and instance access functions. That is the job of the application layer.

Zenject is popular and probably useful. However, I would argue that there are some peculiarities that deviate from the concept of a DI container.