3
\$\begingroup\$

We implemented a small dependency injection mechanism which we use in our project. Now after 2 years of developing our project we need our classes to know who instantiated them via dependency injection. Therefore we pass to the constructor of every class which is "injectable" a property called creator. To achieve this we had two options:

  • create a super-class which every "injectable" has to extend
  • create an interface which every "injectable" has to implement

Both options had downsides but we decided to go with the second option because we didn't want to force classes to extend "injectable" which do not have to do anything with "injection" and only need to extend "injectable" because we can not do multiple inheritance. This lead to the problem that we had to touch every class and clutter them with properties which are only used for DI. Since we use TypeScript we tried to do something with a class decorator, we got the idea from their last example on https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators.

This works quite well but there was the problem, that the creator property was not available in the constructor. Since you can not alter the constructor of an ES6 class we came up with the following solution:

export function injectable(Base) {
    return class extends Base {
        public _creator_;
        constructor(...args) {
            const creator = args.shift();
            Base.prototype._creator_ = creator;
            super(...args);
            Base.prototype._creator_ = null;
            delete Base.prototype._creator_;
            this._creator_ = creator;
        }
    };
}

Now in the DI basically the following happens (this is more "pseudo-code" because we use TypeScript and the original code is a little bit more involved):

// "pseudo-code" because we use TypeScript and the original code is a little bit more involved
class Container {

    static lookup(specifier, requester) {
        const instance = Container._find(specifier, requester._creator_);
        if (instance) {
            return instance;
        }
        // call to injectable is normally done by TypeScript class decorator
        const ExtendedBase = injectable(Container._findClassDefinition(specifier));
        const newInstance = new ExtendedBase(requester._creator_);
        this._set(specifier, newInstance, creator);
    }

    static _find(specifier, creator) {
        // return the instance which belongs to the specifier if it's already created for the given creator
    }

    static _set(specifier, instance, creator) {
        // set the instance of a given specifier for the creator
    }

    static _findClassDefinition(specifier) {
        // find the class definition to a specifier
    }
}

class X {
    constructor() {
        // this is done by a property decorator by TypeScript
        this._injectedService = Container.lookup('injectedService', this);
    }

    run() {
        this._injectedService.doStuff();
    }
}

class Main {
    constructor() {
        this._creator_ = getNextCreator();
        this._x = Container.lookup('x', this._creator_);
    }

    run() {
        this._x.run();
    }
}

const main = new Main();
main.run();

At first glance, this works quite well for us but we are not quite sure if this is really a sophisticated solution or if we can run into troubles if there are concurrent calls to DI.inject(X). We know that JavaScript is not multi-threaded but maybe there are problems with different contexts like WebWorkers or ServiceWorkers etc. We are especially worried because we touch the prototype of the base class. So our question is mostly around the function injectable.

Would be great to get some feedback of you so we can evaluate pros and cons of our approach :-)

EDIT: updated the second code snippet due to @Blindman67's feedback (see the comments of the question)

\$\endgroup\$
  • \$\begingroup\$ Could you clarify the second snippet because as it is it makes no sense... Is the last line meant to be const xInstance = DI.inject(X) and DI.inject thus should be static, or do you mean to reference the created instance di as in const xInstance = di.inject(X); \$\endgroup\$ – Blindman67 Nov 8 '18 at 12:44
  • \$\begingroup\$ @Blindman67 thanks for the hint, that's a typo, I corrected it. Of course it should be di.inject(X). This is why the second snippet is "pseudo" code since it's not copy&paste from our real world application \$\endgroup\$ – stephan.niedermayr Nov 8 '18 at 13:01
  • \$\begingroup\$ Sorry this is still confusing, The Object X do you want it to have access to _creator_ when you call its constructor via super in the Base constructor? \$\endgroup\$ – Blindman67 Nov 8 '18 at 13:20
  • \$\begingroup\$ @Blindman67 yeah inside the constructor of X it could happen that creator is needed. I'll try to update the example for better understanding \$\endgroup\$ – stephan.niedermayr Nov 8 '18 at 13:36
  • \$\begingroup\$ @Blindman67 I totally modified the second snippet. Could you check if it makes more sense for you now? \$\endgroup\$ – stephan.niedermayr Nov 8 '18 at 14:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.