inversify / InversifyJS

A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript.
http://inversify.io/
MIT License
11.35k stars 719 forks source link

Injection of generics #208

Closed davidstellini closed 8 years ago

davidstellini commented 8 years ago

Hi,

I have another question on how to inject generics, possibly using a pattern similar to:

kernel.bind<IFactory<IKatana>>("IFactory<IKatana>")

I've written a short example below:


import {Kernel, injectable, inject} from "inversify"
interface IParser<T> {
    ParseList(objType: { new(): T; }, jsonString: string) : List<T>;
    Parse(objType: { new(): T; }, json: any);
}

interface IModel {}
class Dog implements IModel {}
class Human implements IModel {}

@injectable()
class ApiParser<T> implements IParser<T> {
  ParseList(objType: { new(): T; }, jsonString : string): List<T> {return null;}
  Parse(objType: { new(): T; }, jsonString : string){return null;}
}

abstract class DataRepository<T> {
  parser : IParser<T>;
}

class ApiDogDataRepository extends DataRepository<Dog> {
  constructor(
    @inject('IParser<Dog>') parser : IParser<Dog>
  ) {
    super();
    this.parser = parser; //Need to inject an instance of ApiParser<Dog> here
  }
}

class ApiHumanDataRepository extends DataRepository<Human> {
  constructor(
    @inject('IParser<Human>') parser : IParser<Human>
  ) {
    super();
    this.parser = parser; //Need to inject an instance of ApiParser<Human> here
  }
}

let kernel = new Kernel();
//I need to do something like: kernel.bind<IParser<Human>>("IParser<Human>").to('ApiParser<Human>');
remojansen commented 8 years ago

Hi, can you please try the following:

@injectable() // Needs to be injectable
class ApiDogDataRepository extends DataRepository<Dog> {
  constructor(
    @inject('IParser<Dog>') parser : IParser<Dog>
  ) {
    super();
    this.parser = parser;
  }
}

@injectable() // Needs to be injectable
class ApiHumanDataRepository extends DataRepository<Human> {
  constructor(
    @inject('IParser<Human>') parser : IParser<Human>
  ) {
    super();
    this.parser = parser;
  }
}

let kernel = new Kernel();

kernel.bind<DataRepository<Human>>("DataRepository<Human>")
      .to(ApiHumanDataRepository);

kernel.bind<DataRepository<Dog>>("DataRepository<Dog>")
      .to(ApiDogDataRepository);

kernel.bind<IParser<Human>>("IParser<Human>")
      .to(ApiParser);

kernel.bind<IParser<Dog>>("IParser<Dog>")
      .to(ApiParser);

I haven't run this code so I need your confirmation to know if it works :wink:

remojansen commented 8 years ago

The following should also work:

// ...

@injectable() // Needs to be injectable
class ApiDogDataRepository extends DataRepository<Dog> {
  constructor(
    @inject('IParser') parser : IParser<Dog>
  ) {
    super();
    this.parser = parser;
  }
}

@injectable() // Needs to be injectable
class ApiHumanDataRepository extends DataRepository<Human> {
  constructor(
    @inject('IParser') parser : IParser<Human>
  ) {
    super();
    this.parser = parser;
  }
}

let kernel = new Kernel();

kernel.bind<DataRepository<Human>>("DataRepository<Human>")
      .to(ApiHumanDataRepository);

kernel.bind<DataRepository<Dog>>("DataRepository<Dog>")
      .to(ApiDogDataRepository);

kernel.bind<IParser<any>>("IParser").to(ApiParser);

If your ApiHumanDataRepository and ApiDogDataRepository are doing the same they could also become a generic type ApiDataRepository:

// ...

@injectable() 
class ApiDataRepository<T> extends DataRepository<T> {
  constructor(
    @inject('IParser') parser : IParser<T>
  ) {
    super();
    this.parser = parser;
  }
}

let kernel = new Kernel();

kernel.bind<DataRepository<any>>("IDataRepository").to(ApiDataRepository);
kernel.bind<IParser<any>>("IParser").to(ApiParser);

let apiHumanDataRepository = kernel.get<DataRepository<Human>>("IDataRepository");
let apiDogDataRepository = kernel.get<DataRepository<Dog>>("IDataRepository");
remojansen commented 8 years ago

Hi @Davste93 please confirm if this issue can be closed when you have a chance 😉

davidstellini commented 8 years ago

@remojansen Thanks! that clears up a lot :) Can I choose to bind a particular parser to each different repository?

Ex: bind SomeParser1 (implements Parser) -> ApiDogDataRepository bind SomeParser2 (implements Parser) -> ApiHumanDataRepository

This would be like your Warrior example, but extending it to:

interface IWeapon {}
abstract class Soldier {
weapon : IWeapon;
    constructor(
      @inject('Weapon') weapon : IWeapon;
    ) {
      super();
      this.weapon = weapon;
    }
}
class Archer extends Soldier {}
class Knight extends Soldier {}
@injectable()
class Sword implements IWeapon{}
@injectable()
class Bow implements IWeapon{}
@injectable()
class DefaultWeapon implements IWeapon{}

kernel.bind<IWeapon>("IWeapon").to(DefaultWeapon);
//And then,
kernel.bind /*Knight.weapon */ to Sword
kernel.bind /*Archer.weapon */ to Bow

I want to bind it in a way that getting an instance of knight will create an instance of Sword, however knight should have no knowledge on how to create the Sword instance (thus the use of inversify and not type instantiation via constructor). I might choose to bind 'Mace' instead so that every time I create an instance of a knight it would have a mace Weapon.

My particular real world scenario is that I can't always use the same parser across multiple data repositories, but at the same time, most of the time it IS the same parser. So I can have a JSON parser used across 95% of the app, but 5% would use a HATEOAS parser.

remojansen commented 8 years ago

What you are looking for is a feature called contextual bindings:

interface IWeapon {}

abstract class BaseSoldier {
    weapon : IWeapon;
    constructor(
      weapon : IWeapon
    ) {
      this.weapon = weapon;
    }
}

@injectable()
class Soldier extends BaseSoldier {
    constructor(
      @inject('IWeapon') weapon : IWeapon
    ) {
      super(weapon);
    }
}

@injectable()
class Archer extends BaseSoldier {
    constructor(
      @inject('IWeapon') weapon : IWeapon
    ) {
      super(weapon);
    }
}

@injectable()
class Knight extends BaseSoldier {
    constructor(
      @inject('IWeapon') weapon : IWeapon
    ) {
      super(weapon);
    }
}

@injectable()
class Sword implements IWeapon{}

@injectable()
class Bow implements IWeapon{}

@injectable()
class DefaultWeapon implements IWeapon{}

let kernel = new Kernel();

kernel.bind<IWeapon>("IWeapon").to(DefaultWeapon).whenInjectedInto(Soldier);
kernel.bind<IWeapon>("IWeapon").to(Sword).whenInjectedInto(Knight);
kernel.bind<IWeapon>("IWeapon").to(Bow).whenInjectedInto(Archer);
kernel.bind<BaseSoldier>("BaseSoldier").to(Soldier).whenTargetNamed("default");
kernel.bind<BaseSoldier>("BaseSoldier").to(Knight).whenTargetNamed("knight");
kernel.bind<BaseSoldier>("BaseSoldier").to(Archer).whenTargetNamed("archer");

let soldier = kernel.getNamed<BaseSoldier>("BaseSoldier", "default");
let knight = kernel.getNamed<BaseSoldier>("BaseSoldier", "knight");
let archer = kernel.getNamed<BaseSoldier>("BaseSoldier", "archer");

console.log(knight.weapon);
console.log(archer.weapon);

I had to change your code because it is not possible to create an instance of an abstract class. This means that you will never be able to do something like:

kernel.bind<X>("X").to(SomeAbstractClass).

Also there is a limitation that forces you to indicate the injections in the derived class not in the abstract class so the following won't work:

abstract class BaseSoldier {
    weapon : IWeapon;
    constructor(
      @inject('IWeapon') weapon : IWeapon
    ) {
      this.weapon = weapon;
    }
}

@injectable()
class Soldier extends BaseSoldier {}

But the following will work:

abstract class BaseSoldier {
    weapon : IWeapon;
    constructor(
      weapon : IWeapon
    ) {
      this.weapon = weapon;
    }
}

@injectable()
class Soldier extends BaseSoldier {
    constructor(
      @inject('IWeapon') weapon : IWeapon
    ) {
      super(weapon);
    }
}

So you should be able to do what you want using contextual constraints but I have discovered a bug. If you decorate with injectable the abstract class:

@injectable()
abstract class BaseSoldier {
    weapon : IWeapon;
    constructor(
      @inject('IWeapon') weapon : IWeapon
    ) {
      this.weapon = weapon;
    }
}

A friendly error is displayed:

Error: Derived class must explicitly declare its constructor: Soldier.

But if you forget @injectable() undefined is infected and there are no errors displayed. I'm going to create a new issue to investigate this.

remojansen commented 8 years ago

I have created the issue https://github.com/inversify/InversifyJS/issues/212

davidstellini commented 8 years ago

Found another way, this solves this issue for me: It compiles this time! :P Thanks for the beautiful library, I enjoy using it!

import {injectable, inject, Kernel, IKernel, IRequest} from "inversify";
import "reflect-metadata";

interface Parser<T>{
  parse() : string;
}

class Dog {
  name : string;
}
class Human{
  surname : string;
}

@injectable()
class HateosParser<T> implements Parser<T> {
  parse() : string {
    return "Hello HATEOAS!";
  }
}

@injectable()
class ApiParser<T> implements Parser<T> {
  parse() : string {
    return "Hello JSON!";
  }
}

abstract class DataRepository<T> {
  parser : Parser<T>;
}

@injectable()
class DogDataRepository extends DataRepository<Dog> {

  constructor(
    @inject('Parser') parser : Parser<Dog>
  ) {
    super();
    this.parser = parser;
  }
}

@injectable()
class HumanDataRepository extends DataRepository<Human> {
  constructor(
    @inject('Parser')  parser : Parser<Human>
  ) {
    super();
    this.parser = parser;
  }
}

let kernel : IKernel = new Kernel();

kernel.bind<DogDataRepository>("DogDataRepository").to(DogDataRepository);
kernel.bind<HumanDataRepository>("HumanDataRepository").to(HumanDataRepository);

kernel.bind<Parser<any>>("Parser").to(HateosParser).when((request: IRequest) => {
    return request.parentRequest.serviceIdentifier === 'DogDataRepository';
});

kernel.bind<Parser<any>>("Parser").to(ApiParser).when((request: IRequest) => {
    return request.parentRequest.serviceIdentifier !== 'DogDataRepository';
});

var dogDataRepository = kernel.get<DogDataRepository>("DogDataRepository");
var humanDataRepository = kernel.get<HumanDataRepository>("HumanDataRepository");

console.log(dogDataRepository.parser.parse());
console.log(humanDataRepository.parser.parse());
remojansen commented 8 years ago

Great :tada: I was about to send another example a bit more simple:

let kernel = new Kernel();

kernel.bind<IWeapon>("IWeapon").to(DefaultWeapon).whenInjectedInto(Soldier);
kernel.bind<IWeapon>("IWeapon").to(Sword).whenInjectedInto(Knight);
kernel.bind<IWeapon>("IWeapon").to(Bow).whenInjectedInto(Archer);
kernel.bind<BaseSoldier>("Soldier").to(Soldier);
kernel.bind<BaseSoldier>("Knight").to(Knight);
kernel.bind<BaseSoldier>("Archer").to(Archer);

let soldier = kernel.get<BaseSoldier>("Soldier");
let knight = kernel.get<BaseSoldier>("Knight");
let archer = kernel.get<BaseSoldier>("Archer");

console.log(soldier.weapon);
console.log(knight.weapon);
console.log(archer.weapon);

The thing about contextual bindings is that is a really flexible feature so it is easy to find alternative and better ways :smile:

Can I close this issue then?

davidstellini commented 8 years ago

Go ahead! Thank you!

alexey-pelykh commented 6 years ago

This is still the case, unless skipBaseClassChecks is set to true. Is there a way to tag specific class to be ignored from checks?

gentunian commented 4 years ago

I'm trying to find out how to inject a specific class for a generic interface that are similar to some examples above, but I think that this is not possible. Any help would be much appreciate it.

interface DataProvider<T> {}

@injectable()
class StringDataProvider implements DataProvider<String> {}

@injectable()
class NumberDataProvider implements DataProvider<Number> {}

@injectable()
class DataWrapper<T> {
  constructor(dataProvider: DataProvider<T>) {}
}

// expecting this to work to create DataWrapper object with NumberDataProvider injected
container.get<DataWrapper<Number>>(SomeClassifier)

// expecting this to work to create DataWrapper object with StringDataProvider injected
container.get<DataWrapper<String>>(SomeClassifier)

I've tried what it's been already discussed here but my use case seems different. This comment always returns ApiParser and it's not what I want in my example.

What I don't understand is why this is ambiguous:

container.bind<DataProvider<Number>>("dataprovider").to(NumberDataProvider)
container.bind<DataProvider<String>>("dataprovider").to(StringDataProvider)

The classifier for the service is the same but the inferred types are differents.

Any help and/or guidance on this?