Skip to content

Proposal: add support for dynamic parameters #177

Description

@lazyoft

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.

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").toValue(new Date());

let subject = kernel.get<IUseDate>("IUseDate");
subject.doSomething(); // will print the same date over and over

In this case I will have to bind Date with a toValue, because by default it is not decorated as injectable, therefore even though my IUseDate 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 there

@injectable()
class UseDate implements IUseDate {
    constructor(@inject("Date") private currentDate: IFactory<Date>) { 
    }

    doSomething() {
        // since it is IFactory I will have to create this
        console.log(`Today is ${this.currentDate()}`);
    }
}

...

kernel.bind<IFactory<Date>>("Date").toFactory(context => {
       return () => new Date();
});

whereas 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.

kernel.bind<IUseDate>("IUseDate").to(UseDate).withDynamicParameters((context, params) => {
     params["Date"] = new Date();
});

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:

interface ISword {
    hit();
}

@injectable()
class MetallicSword implements ISword {
    hit() {
        console.log("Cutting something");
    }
}

@injectable()
class ToySword implements ISword {
    hit() {
        console.log("Pretending to cut something");
    }
}

@injectable()
class EnchantedSword implements ISword {
    constructor(@inject("ISword") private baseSword: ISword) {
    }

    hit() {
        console.log("Sword is enchanted!");
        this.baseSword.hit();
    }
}

let kernel = new Kernel();
kernel.bind<ISword>("ISword").to(MetallicSword);
kernel.bind<ISword>("ISword").to(ToySword);
kernel.bind<ISword>("ISword").to(EnchantedSword);

let sword = kernel.get<ISword>("ISword"); // an EnchantedSword would be nice here...
sword.hit();

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.

kernel.bind<ISword>("ISword").to(MetallicSword).whenTargetNamed("real");
kernel.bind<ISword>("ISword").to(ToySword).whenTargetNamed("toy");
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);
});

let sword = kernel.get<ISword>("ISword"); // Will return the enchanted sword based on the player
sword.hit();

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions