Closed lazyoft closed 8 years ago
In this example:
kernel.bind<ISword>("ISword").to(EnchantedSword).withDynamicParameters((context, params) => {
// Imagine to have some logic here that picks the kind of sword to enchant
let swordType = isWarrior(player) ? "real" : "toy"
params["ISword"] = context.kernel.getNamed<ISword>("ISword", swordType);
});
My understanding is that player
and isWarrior
are somehow available as globals. The fact they are not passed to the function as arguments is something that looks like a code smell to me.
We want to achieve this without using globals. The problem is that there are two different cases here:
If you are trying to do something like:
function isWarrior(player) {
return player instanceof Warrior; // looking for types
}
Then you cal use contextual constrains to remove the globals.
kernel.bind<ISword>("ISword").to(MetallicSword).whenInjectedInto(Warriot);
kernel.bind<ISword>("ISword").to(ToySword).whenInjectedInto(Civil);
// or
kernel.bind<ISword>("ISword").to(MetallicSword).whenAnyAncestorIs(Warrior);
kernel.bind<ISword>("ISword").to(ToySword).whenAnyAncestorIs(Civil);
It would also be possible to use metadata:
kernel.bind<ISword>("ISword").to(MetallicSword).whenAnyAncestorTagged("warrior", true);
kernel.bind<ISword>("ISword").to(ToySword).whenAnyAncestorTagged("warrior", false);
Take a look to this example of contextual constraints I wrote it some time ago but is related:
There are many out of the box constraints available that you could potentially use:
interface IBindingWhenSyntax<T> {
when(constraint: (request: IRequest) => boolean): IBindingOnSyntax<T>;
whenTargetNamed(name: string): IBindingOnSyntax<T>;
whenTargetTagged(tag: string, value: any): IBindingOnSyntax<T>;
whenInjectedInto(parent: (Function|string)): IBindingOnSyntax<T>;
whenParentNamed(name: string): IBindingOnSyntax<T>;
whenParentTagged(tag: string, value: any): IBindingOnSyntax<T>;
whenAnyAncestorIs(ancestor: (Function|string)): IBindingOnSyntax<T>;
whenNoAncestorIs(ancestor: (Function|string)): IBindingOnSyntax<T>;
whenAnyAncestorNamed(name: string): IBindingOnSyntax<T>;
whenAnyAncestorTagged(tag: string, value: any): IBindingOnSyntax<T>;
whenNoAncestorNamed(name: string): IBindingOnSyntax<T>;
whenNoAncestorTagged(tag: string, value: any): IBindingOnSyntax<T>;
whenAnyAncestorMatches(constraint: (request: IRequest) => boolean): IBindingOnSyntax<T>;
whenNoAncestorMatches(constraint: (request: IRequest) => boolean): IBindingOnSyntax<T>;
}
If you are looking for something like:
function isWarrior(player) {
return player.isWarrior; // looking for runtime data
}
In this case we need a factory to remove the globals because you need to pass the data as arguments.
kernel.bind<IFactory<ISword>>("IFactory<ISword>").toFactory<ISword>((context) => {
return (player) => {
if (player.isWarrior) {
return context.kernel.getNamed<ISword>("ISword", "real");
} else {
return context.kernel.getNamed<ISword>("ISword", "toy");
}
};
});
We can't do what castle does because our architecture is different. However, I think there is a use case that is not available right now.
interface IUseDate {
doSomething();
}
@injectable()
class UseDate implements IUseDate {
constructor(@inject("Date") private currentDate: Date) {
}
doSomething() {
console.log(`Today is ${this.currentDate}`);
}
}
let kernel = new Kernel();
kernel.bind<IUseDate>("IUseDate").to(UseDate);
kernel.bind<Date>("Date").toDynamicValue((context) => { new Date(); });
let subject = kernel.get<IUseDate>("IUseDate");
subject.doSomething(); // will print a new date in each instance
withDynamicParameters
for now because it doesn't fit well with the InversifyJS architecture and there are potential ways to work around it.toDynamicValue
to solve the date use case.Hi @remojansen, Thanks for the explanation, I didn't pay a lot of attention to binding constraints honestly, but they seem quite a powerful mechanism that should solve a lot of the issues I presented. The specific case I was trying to solve was that of an abstract factory that could use the kernel in order to solve components.
interface ICarFactory {
createEngine(displacement: number): IEngine;
createTires(code: string): ITires;
createBody(color: string): IBody;
}
@injectable()
class DieselEngine implements IEngine {
constructor(
@inject("IInjectorPump") private injectorPump: IInjectorPump,
@inject("displacement") private displacement: number) {}
/*...*/
}
@injectable()
class PetrolEngine implements IEngine {
constructor(
@inject("ISparkPlugs") private sparkPlugs: ISparkPlugs,
@inject("displacement") private displacement: number) {}
/*...*/
}
In this case the end user might be able to create its own engine, without having to worry about the other parts of the car but the displacement. As engines are dependent on different parts which depend on their type I just wanted the end user to provide one or few parameters that might change dynamically (depending on the choice made). From what I understood this cannot be solved by the toProvider, because it will force me to resolve all the dependencies manually from within the closure. On the other hand I think that toDynamicValue might solve this specific issue more easily, because it allows me to bind that specific dependency to a closure that will capture the value given by the user without having to wrap my values in an IProvider<T>
interface.
Hi, thanks for your comment. If the users invoke:
let engine = carFactory.createEngine(1);
They factory creates a new engine with displacement === 1
but how does the factory knows if the engine should be PetrolEngine
or DieselEngine
?
Hi @lazyoft please take a look to the following. I have assumed that you need some sort of "engine name":
// this bindings should not be exposed to the users
let kernel = new Kernel();
kernel.bind<ISparkPlugs>("ISparkPlugs").to(SparkPlugs);
kernel.bind<IInjectorPump>("IInjectorPump").to(InjectorPump);
kernel.bind<IFactory<IEngine>>("IFactory<IEngine>").toFactory<IEngine>((context) => {
return (named: string) => {
return context.kernel.getNamed<IEngine>("IEngine", named);
};
});
// expose this function to the users of the library (do not expose the kernel directly)
function registerEngine(engine, named, displacement) {
// bind the engine to a category using a named binding
kernel.bind<IEngine>("IEngine").to(engine).whenTargetNamed(named);
// biding the displacement to the engine that we just registered
kernel.bind<number>("displacement").to(displacement).whenInjectedInto(engine);
}
// users can register new engines
registerEngine(DieselEngine, "diesel", 1);
registerEngine(PetrolEngine, "petrol", 2);
let createEngine = kernel.get<IFactory<IEngine>>("IFactory<IEngine>");
let dieselEngine = createEngine("diesel"); // displacement 1
let petrolEngine = createEngine("petrol"); // displacement 2
The only problem with this is if you have two engines with the same name... but you could bind engines to cars or something more advanced:
function registerEngine(engine: IEngine, car: ICar, displacement: number) {
// bind the engine to a category using a named binding
kernel.bind<IEngine>("IEngine").to(engine).whenInjectedInto(car);
// biding the displacement to the engine that we just registered
kernel.bind<number>("displacement").to(displacement).whenInjectedInto(engine);
}
registerEngine(DieselEngine, SomeCarType, 1);
registerEngine(PetrolEngine, AnotherCarType, 2);
Hi @remojansen, apologies but I am still trying to wrap my head around this, therefore maybe I wasn't very clear with my intent. This will work for binding a specific engine to a name that can later be used for retrieving that, and this is quite nice, but what if I want to build engines of the same type with different displacements?
interface ICarFactory {
createEngine(displacement: number): IEngine;
createTires(code: string): ITires;
createBody(color: string): IBody;
}
// This will internally register and create diesel engines
class DieselCarFactory implements ICarFactory {
createEngine(displacement: number): IEngine { // How can I pass the displacement to the factory?
/*...*/
}
}
From what I understood in order to do that as of now, I can unbind and rebind the displacement binding from within the createEngine
method, and assigning it to the current displacement value. I don't think I can do this using contextual constraints, because the constraints for building the engine will always be the same, with the displacement being the only thing changing. From what is my understanding having a toDynamicValue
in this scenario will help, because in that case I can simply close to a local variable that will contain the displacement at the time of creation.
class DieselCarFactory implements ICarFactory {
private currentDisplacement: number;
constructor(private kernel: IKernel) {
kernel.bind<number>("displacement").toDynamicValue(context => this.currentDisplacement);
}
createEngine(displacement: number): IEngine {
this.currentDisplacement = displacement; // good thing we don't have threads...
return this.kernel.getNamed<IEngine>("IEngine", "diesel");
}
}
thanks again for your support. I can work around this by now, so no need to hurry with the toDynamicValue, even though it might be helpful on this and other cases IMHO.
Thanks a lot for helping me to understand your needs. I've been doing a lot of thinking about the different ways we could try to resolve this use case and I think I have found a nice solution:
// Current IFactory behaviour
export interface IFactory<T> extends Function {
(...args: any[]): T;
}
// Proposed IFactory behaviour
export interface IFactory<T> extends Function {
(...args: any[]): (IFactory<T>|T);
}
We will need to change a bit the classes:
@injectable()
class DieselEngine implements IEngine {
private _injectorPump: IInjectorPump;
public displacement: number;
constructor(
@inject("IInjectorPump") injectorPump: IInjectorPump
) {
this._injectorPump = injectorPump;
this.displacement = null; // don't inject displacement
}
/*...*/
}
@injectable()
class PetrolEngine implements IEngine {
private _sparkPlugs: ISparkPlugs
public displacement: number;
constructor(
@inject("ISparkPlugs") sparkPlugs: ISparkPlugs
) {
this._sparkPlugs = sparkPlugs;
this.displacement = null; // don't inject displacement
}
/*...*/
}
The factory will work with AMBIGUOUS_MATCH
:
kernel.bind<ISparkPlugs>("ISparkPlugs").to(SparkPlugs);
kernel.bind<IInjectorPump>("IInjectorPump").to(InjectorPump);
kernel.bind<IEngine>("IEngine").to(PetrolEngine).whenTargetNamed("petrol");
kernel.bind<IEngine>("IEngine").to(DieselEngine).whenTargetNamed("diesel");
kernel.bind<IFactory<IEngine>>("IFactory<IEngine>").toFactory<IEngine>((context) => {
return (named: string) => (displacement: number) => {
let engine = context.kernel.getNamed<IEngine>("IEngine", named);
engine.displacement = displacement;
return engine;
};
});
@injectable()
class DieselCarFactory implements ICarFactory {
private _engineFactory: IFactory<IEngine>;
constructor(
@inject("IFactory<IEngine>") factory: IFactory<IEngine> // (...args: any[]) => (...args: any[]) => T;
) {
this.factory = factory("diesel"); // (...args: any[]) => T;
}
createEngine(displacement: number): IEngine {
return this.factory(displacement); // T
}
}
The factory will also work with NO AMBIGUOUS_MATCH
:
kernel.bind<IEngine>("IEngine").to(DieselEngine);
kernel.bind<IFactory<IEngine>>("IFactory<IEngine>").toFactory<IEngine>((context) => {
return (displacement: number) => {
let engine = context.kernel.get<IEngine>("IEngine");
engine.displacement = displacement;
return engine;
};
});
@injectable()
class DieselCarFactory implements ICarFactory {
private _engineFactory: IFactory<IEngine>;
constructor(
@inject("IFactory<IEngine>") factory: IFactory<IEngine> // (...args: any[]) => T;
) {
this.factory = factory; // (...args: any[]) => T;
}
createEngine(displacement: number): IEngine {
return this.factory(displacement); // T
}
}
My full notes are available here.
I like this, it looks cleaner than the other solutions and doesn't force the user to make the parameters the factory is going to define injectable. The only minor drawback I can think of is the exposition of the parameter as a public property, which might not be desirable in all the use cases, but it looks like a fair tradeoff to me, given the flexibility it provides. Thanks a lot!
Cool! I'm going to close this issue and create two separated issues one for the toDynamicValue
and another for the new IFactory<T>
signature.
I know using castings is no the best but If you really wanted to keep displacement
private you would be able to do the following:
@injectable()
class DieselEngine implements IEngine {
private _injectorPump: IInjectorPump;
private _displacement: number;
constructor(
@inject("IInjectorPump") injectorPump: IInjectorPump
) {
this._injectorPump = injectorPump;
this._displacement = null; // don't inject displacement
}
/*...*/
}
kernel.bind<IFactory<IEngine>>("IFactory<IEngine>").toFactory<IEngine>((context) => {
return (named: string) => (displacement: number) => {
let engine = context.kernel.getNamed<IEngine>("IEngine", named);
(<any>engine)._displacement = displacement; // note casting here
return engine;
};
});
Context
Consider the case in which I want to build an object whose dependencies might change over time. For instance let's say I want an object that requires the current date.
In this case I will have to bind Date with a
toValue
, because by default it is not decorated as injectable, therefore even though myIUseDate
class is transient it will only show the same date for every instance.A workaround could be to use the
toFactory
method, and build from therewhereas this might work most of the times it is not optimal because it forces me to declare my dependencies as
IFactory<T>
, and to create them from within the class.Proposed solution
It would be nice to add a
withDynamicParameters
method that can alter the binding of a component by changing the resolution of its own dependencies.this way the system can refer to this block of code in order to resolve the dependencies. This might come in handy also in those cases for which I will have to change the behavior of the component based on some external condition, as it could allow me to overwrite previous dependencies.
Fields of application
Besides dependencies whose value might change over time this might come in handy for handling decorators whose base class might change depending on some external context:
The previous example will throw an error because there are multiple declarations for ISword, even though the application might most likely use just one of the swords declared on the kernel and enchant it. With dynamic parameters the scenario can be handled by overriding the dependency based on some external criteria.
From what I can see by reading the library something similar is possible with the activation handler, which is used to decorate functions and types after they are already built. Maybe it is possible to do something similar before the type gets created, but I haven't looked at the source code so thoroughly right now.