vovaspace / brandi

The dependency injection container powered by TypeScript.
https://brandi.js.org
ISC License
193 stars 12 forks source link

Array Injections #27

Closed synkarius closed 1 year ago

synkarius commented 2 years ago

First of all, let me say that this is an awesome library and I'm enjoying using it very much.

This is a feature request. If I'm understanding the docs correctly, currently, if I want to inject a class with an array of things, I have to do something like this to get the array injectable:

class ArrayOfStrings extends Array<string> {}
container.bind(Tokens.TestString).toConstant('hello');
container.bind(Tokens.TestString2).toConstant('brandi.js');
container.bind(Tokens.TestArray).toInstance(ArrayOfStrings).inSingletonScope();
injected(ArrayOfStrings, Tokens.TestString, Tokens.TestString2);
injected(SomeOtherClass, Tokens.TestArray);

This gets the job done, but it would be nice if there were a smoother mechanism. In particular, this line is kind of ugly:

injected(ArrayOfStrings, Tokens.TestString, Tokens.TestString2);

I don't want to be too specific about how that gets accomplished except to say that specifying the precise tokens of the injected array items is maybe less slick than marking them all somehow and then providing the marker to injected or some function like it.

polerin commented 1 year ago

@synkarius I agree that it'd be great to have a built in solution for this, it's something I miss from Zenject. I do have a slightly different approach to temporally solve this that might be useful to you:

const tokens = {
  listToken: token<IExample[]>('IExample[]'),
  e1: token<IExample>('example1'),
  e2: token<IExample>('example2'),
  e3: token<IExample>('example3'),
};

const demoContainer = new Container();

// bind e1, e2, e3 however

demoContainer.bind(listToken)
  .toInstance(() => [
    demoContainer.get(tokens.e1),
    demoContainer.get(tokens.e2),
    demoContainer.get(tokens.e3),
  ])
  .inSingletonScope();  //or whatever appropriate

This is still a bit imperfect, but it feels a bit more connected. The one thing that would make this a lot better is if factory methods received the requesting container, because it would allow for more flexible definition of factories in dependency modules. Currently there doesn't seem to be an easy way to do this kind of construction with demoContainer being a demoModule and the container is elsewhere.

I don't know if it is feasible for the way brandi does things, but the way Zenject handles this is to allow multiple things to be bound to the same requirement (basically bound to the same token). If you request an IExample, it'd just give you the first of them (I think.. it's been a second) but if you ask for a List<IExample> it collects all matching bound items and supplies them. Super duper handy for injecting collected strategies and adapters into a consuming bit of business logic.

synkarius commented 1 year ago

@polerin The thing I don't like about this solution is that it requires tokens e1-e3 to be bound before listToken can be bound, which partially defeats the purpose of using a DI lib. (After all, couldn't you just use constructor injection with no DI lib if you're forced to bind things in order?)

The Zenject 2nd-binding-to-token-is-array thing sounds pretty clean though.

polerin commented 1 year ago

@polerin The thing I don't like about this solution is that it requires tokens e1-e3 to be bound before listToken can be bound, which partially defeats the purpose of using a DI lib. (After all, couldn't you just use constructor injection with no DI lib if you're forced to bind things in order?)

The Zenject 2nd-binding-to-token-is-array thing sounds pretty clean though.

Technically e1..e3 just have to be bound before the container.get(listToken) call is made, as toInstance() is evaluated lazily by default.

synkarius commented 1 year ago

Ah. I didn't realize toInstance was evaluated lazily. That seems fine then.