microsoft / tsyringe

Lightweight dependency injection container for JavaScript/TypeScript
MIT License
5.19k stars 171 forks source link

container.registerDecorator() Feature Request #133

Open panzerdp opened 4 years ago

panzerdp commented 4 years ago

Hello! Thank you for such a wonderful library.

To leverage the decorator pattern, I need to inject different Implementations of the same Interface into the Decorator.

For example.

1) The service interface:

interface CommandHandler {
  execute(): void;
}

2) Multiple services implementations:

class CommandHandlerImpl1 implements CommandHandler  {
  execute(): void {
    console.log('Implementation 1');
  }
}

class CommandHandlerImpl2 implements CommandHandler {
  execute(): void {
    console.log('Implementation 2');
  }
}

3) The decorator class:

@injectable()
class CommandHandlerDecorator implements CommandHandler {
  constructor(@inject('CommandHandler') private decoratee: CommandHandler) {}

  execute(): void {
    console.log('Decorator');
    this.decoratee.execute();
  }
}

Using Pure DI, I can decorate and instantiate the 2 implementations as such:

const commandHandler1 = new CommandHandlerDecorator(
  new CommandHandlerImpl1()
);
const commandHandler2 = new CommandHandlerDecorator(
  new CommandHandlerImpl2()
);

But how can it be implemented using tsyringe?

container.register('CommandHandler', {
  useClass: CommandHandlerIml1
});
container.register('CommandHandlerDecorated1', {
  useClass: CommandHandlerDecorator
});

const commandHandler1 = container.resolve('CommandHandlerDecorated1');

// How to register the decorated CommandHandlerIml2?

My guess is that tsyringe needs a container.registerDecorator() (like Simple Injector does) to handle situations like these.

Thank you.

MeltingMosaic commented 4 years ago

Hmm, interesting. Let me see if I understand this correctly - you're looking to do something like:

const cmdHandler1 = container.resolve('CommandHandlerDecorated1'); // returns the moral equivalent of new CommandHandlerDecorator(container.resolve('CommandHandlerIml1'))

const cmdHandler2 = container.resolve('CommandHandlerDecorated2'); // returns the moral equivalent of new CommandHandlerDecorator(container.resolve('CommandHandlerIml2'))

Unfortunately, there's no way to pass a discriminator into the resolution of CommandHandlerDecorator which would allow it to wrap the correct CommandHandler.

I think you can do this with a factory, but probably not through regular resolution. Could you write an example of your ideal calling pattern if we were to implement registerDecorator()?

panzerdp commented 4 years ago

Let me see if I understand this correctly - you're looking to do something like

Yes, I'm looking for a way to resolve decorated implementations.

I think you can do this with a factory, but probably not through regular resolution. Could you write an example of your ideal calling pattern if we were to implement registerDecorator()?

Here's a possibility:

container.register<CommandHandler>(CommandHandlerImpl1, CommandHandlerImpl1);
container.register<CommandHandler>(CommandHandlerImpl2, CommandHandlerImpl2);
container.register<CommandHandler>(CommandHandlerDecorator, CommandHandlerDecorator);

container.registerDecorator(
  CommandHandlerImpl1,  // the decoratee
  CommandHandlerDecorator, // the decorator
  'CommandHandler' // the name of the prop inside the decorator
);

container.registerDecorator(
  CommandHandlerImpl2, // the decoratee
  CommandHandlerDecorator, // the decorator
  'CommandHandler' // the name of the prop inside the decorator
);

const cmdHandler1 = container.resolve(CommandHandlerImpl1);
const cmdHandler2 = container.resolve(CommandHandlerImpl2);

Please take a look here how decorators are registered in SimpleInjector (C#).

MeltingMosaic commented 4 years ago

How are those cmdHandlers used? If they are resolved as constructor params elsewhere, there is a way to do it (I think), by using the @injectWithTransform() decorator. So you might be able to do something like

@injectable()
class CommandUser {
  constructor(@injectWithTransform(CommandHandlerImpl1, CommandHandlerTransform) decoratedCommandHandler: CommandHandler){
    // ...
  }
}

Where you'd define the CommandHandlerTransform as something like:

class CommandHanderTransform implements Transform <CommandHandler, CommandHandler>{
  public transform(cmdHandler: CommandHandler): CommandHandler {
    return new CommandHandlerDecorator(cmdHandler);
  }
}

This won't work as written if CommandHandlerDecorator itself has dependencies that need to be resolved from the container. In that case, you would need to make CommandHandlerDecorator @autoInjectable() so that you could pass the original cmdHandler into it and then have the dependencies resolved.

Another possibility is to add a new feature to extend the transform behavior on explicit resolution, where we could have something like container.resolveWithTransform<TIn, TOut>(<TIn>, Transform<TIn, Tout>): TOut, which would use the existing Transform behavior.

panzerdp commented 4 years ago

How are those cmdHandlers used?

1) I can either use them directly into a component:

function MyComponent() {
  const handle = () => { 
    const myCommand = container.resolve(CommandHandlerImpl1);
    myCommand.execute();
  };
  return <button onClick={handle}></button>
}

2) Or supply as dependencies to other service:

@injectable()
class MyService {
  constructor(private cmd1: CommandHandlerImpl1, private cmd2: CommandHandlerImpl2) {}

  doSomething(flag) {
    if (flag) {
      this.cmd1.execute();
    } else {
      this.cmd2.execute();
    }
  }
}

That's why I'd like to register the decorator on registration phase, and during regular resolving of CommandHandlerImpl1 and CommandHandlerImpl2 the DI should automatically decorate these dependencies with CommandHandlerDecorator.

Also I'd like to register multiple decorators on top of the same implementation:

container.registerDecorator(
  CommandHandlerImpl1,  // the decoratee
  CommandHandlerDecorator1, // the first decorator
  'CommandHandler' // the name of the prop inside the decorator
);

container.registerDecorator(
  CommandHandlerImpl1,  // the decoratee
  CommandHandlerDecorator2, // the second decorator
  'CommandHandler' // the name of the prop inside the decorator
);

// ...

This won't work as written if CommandHandlerDecorator itself has dependencies that need to be resolved from the container.

Yes, I'd like the decorator also to have its dependencies resolved by the container.

The transform solution isn't quite what I'm looking for, since it clutters the resolving of dependencies.

AlexErrant commented 2 years ago

@panzerdp did you ever find a solution to decorator pattern with DI containers in Javascript? I've looked at 5 different DI containers now, and all but one don't support it... and the one that does support it has perf issues once you register more than 20 dependencies.

panzerdp commented 2 years ago

@AlexErrant I just did it manually. :)