Implement it completely. Di-in-js


Hello everyone! Today I’ll try experimenting with Dependency Injection in pure JavaScript . Those who do not know what kind of game it is and how to cook it, I invite you to familiarize yourself. Well, those who are in the know will have an occasion to write an important and useful comment. So, we drove ...


Dependency injection


DI is an architectural pattern that is designed to reduce the connectedness of the system entities - components, modules, classes. The less connected (not to be confused with connectedness ), the easier it is to change these very entities, add new ones and test them. In general, a plus is a plus, but let's see if this is really so.


Without DI:


   class Engine {...};
   class ElectroEngine {...};
   class Transmission {...};
   class Chassis {...};
   class TestChassis {...};

   class Car {
        constructor() {
            this.engine = new Engine();
            this.transmission = new Transmission();
            this.chassis = new Chassis();
        }
    }

    class ElectroCar {
        constructor() {
            this.engine = new ElectroEngine();
            this.transmission = new Transmission();
            this.chassis = new Chassis();
        }
    }

   class TestCar {
        constructor() {
            this.engine = new Engine();
            this.transmission = new Transmission();
            this.chassis = new TestChassis ();
        }
    }

    const car = new Car();
    const electroCar = new ElectroCar();
    const testCar = new TestCar();

With DI:


    class Engine{...};
    class ElectroEngine {...};
    class TestEngine {...};

    class Transmission {...};
    class TestTransmission {...};

    class Chassis {...};
    class SportChassis {...};
    class TestChassis {...};

     class Car {
        constructor(engine, transmission, chassis) {
            this.engine = engine;
            this.transmission = transmission;
            this.chassis = chassis;
        }
    }

    const petrolCar = new Car(new Engine(), new Transmission(), new Chassis());
    const sportCar = new Car(new Engine(), new Transmission(), new SportChassis());
    const electroCar = new Car(new ElectroEngine(), new Transmission(), new Chassis());
    const testCar = new Car(new TestEngine(), new TestTransmission(), new TestChassis());

In the first example without DI, our Car class is tied to specific classes, and therefore, to create, for example, electroCar , you have to make a separate ElectroCar class . In this embodiment, there is a "hard" implementation dependency i.e. Dependence on the instance of a particular class.


β€” DI, Car. . ! β€” . , "" β€” .


DI . "" β€” , , . , ? , . :


class Engine{
   constructor(candles, pistons, oil) {….}
};

class Chassis{
    constructor(doors, hood, trunk) {….}
};

const petrolCar = new Car(
    new Engine(new Candles(), new Pistons(), new Oil() ), 
    new Transmission(…..), 
    new Chassis(new Doors, new Hood(), new Trunk())
);

. , . , , .


Inversion of Control


"" DI- β€” Inversion of Control (IoC). , β€” , . DI, IoC , . - , . :


class Engine{...};
class Transmission{...};
class Chassis{…}

class Car {
        constructor(engine: Engine, transmission: Transmission, chassis: Chassis) {}
} 

const car = new Car();

car.engine instanceof Engine; //*true*

β€” new Car(). β€” , a .


DI-in-JS


JS. , DI . "".


, , , , . . .


constructor(engine = Engine, transmission = Transmission, chassis = Chassis)

:


constructor(engine: Engine, transmission: Transmission, chassis: Chassis)

, . IoC «» , . , ?
, Reflection. , β€” .


, JS:


function reflectionMetaInfo(a) { console.log(a); }

reflectionMetaInfo.name ;       // reflectionMetaInfo;
reflectionMetaInfo.length   ;   //1
reflectionMetaInfo.toString();  //function reflectionMeta(a) { console.log(a);}
arguments;                      //Arguments [%value%/]

, toString(). . , , . () , . , .


const constructorSignature =  classFunc
                                 .toString()
                                 .replace(/\s|['"]/g, '')
                                 .replace(/.*(constructor\((?:\w+=\w+?,?)+\)).*/g, '$1')
                                 .match(/\((.*)\)/)[1]
                                 .split(',')
                                 .map(item => item.split('='));

constructorSignature // [ [dep1Name, dep1Value], [dep2Name, dep2Value] …. ]

, , . , , . . IoC β€” .


:


function Injectable(classFunc, options) {
    const 
        depsRegistry = Injectable.depsRegistry || (Injectable.depsRegistry = {}),
        className = classFunc.name,
        factories = options && options.factories;

    if (factories) {
        Object.keys(factories).forEach(factoryName => {
           depsRegistry[factoryName] = factories[factoryName];
        })
    }

    const depDescriptions = classFunc.toString()
                                .replace(/\s/g, '')
                                .match(/constructor\((.*)[^(]\){/)[1]
                                .replace(/"|'/g, '')
                                .split(',')
                                .map(item => item.split('='));

    const injectableClassFunc = function(...args) {

            const instance = new classFunc(...args);

            depDescriptions.forEach(depDescription => {
                const 
                    depFieldName = depDescription[0],
                    depDesc = depDescription[1];

                if (instance[depFieldName]) return;

                try {
                    instance[depFieldName] = new depsRegistry[depDesc]();
                } catch (err) {
                    instance[depFieldName] = depDesc;
                } 
            });

            return instance;
        }

    return depsRegistry[classFunc.name] = injectableClassFunc;
}

class CustomComponent {
    constructor(name = "Custom Component") {
        this.name = name;
    }
    sayName() {
        alert(this.name);
    }
}

const Button = Injectable(
    class Button extends CustomComponent {
        constructor(name = 'Button') {
            super(name);
        }
    }
)

const Popup = Injectable(
    class Popup extends CustomComponent {
        constructor(
            confirmButton = 'confirmButtonFactory',
            closeButton = Button,
            name = 'NoticePopup'
        ) {
            super(name);
        }
    },
    {
        factories: {
            confirmButtonFactory: function() { return new Button('Confirm Button') }
        }
    }
);

const Panel = Injectable(
    class Panel extends CustomComponent {
        constructor(
            closeButton = 'closeButtonFactory',
            popup = Popup,
            name = 'Head Panel'
        ) {
            super(name);
        }
    },
    {
        factories: {
            closeButtonFactory: function() { return new Button('Close Button') }
        }
    }
);

const customPanel = new Panel();

, , . , . β€” Injectable, IoC. :


  1. ;
  2. ;
  3. ;
  4. ;
  5. ;

, try-catch .


:


  1. . . , , , - .
  2. . , , . - option.factories, .

.


:


function inject(context, ...deps) {
    const 
        depsRegistry = inject.depsRegistry || (inject.depsRegistry = {}),
        className = context.constructor.name;

    let depsNames = depsRegistry[className]; 

    if (!depsNames) {
        depsNames 
            = depsRegistry[className] 
            = context.constructor
                .toString()
                .replace(/\s|['"]/g, '')
                .replace(/.*(inject\((?:\w+,?)+\)).*/g, '$1')
                .replace(/inject\((.*)\)/, '$1')
                .split(',');

       depsNames.shift();
    } 

    deps.forEach((dep, index) => {
        const depName = depsNames[index];
        try {
            context[depName] = new dep();
        } catch (err) {
            context[depName] = dep;
        }
    });

    return context;
}

class Component {

    constructor(name = 'Component') {

        inject(this, name);
    }

    showName() {
        alert(this.name);
    }
}

class Button extends Component {

    constructor(name = 'Component') {
        super();
        inject(this, name);
    }

    disable() {
        alert(`button ${this.name} is disabled`);
    }

    enable() {
        alert(`button ${this.name} is enabled`);
    }
}

class PopupComponent extends Component {

    show() {
        alert(`show ${this.name} popup`);
    }

    hide() {
         alert(`hide ${this.name} popup`);
    }
}

class TopPopup extends PopupComponent {
    constructor(
        popupButton = Button,
        name = 'Top Popup'
    ) {
        super();
        inject(this, popupButton, name);
        this.popupButton.name = 'TopPopup Button';
    }
}

class BottomPopup extends PopupComponent {
    constructor(
        popupButton = function() { return new Button('BottomPopup Button') },
        name = 'Bottom Popup'
    ) {
        super();
        inject(this, popupButton, name);
    }
}

class Panel extends Component {
    constructor(
        name = 'Panel',
        popup1 = TopPopup,
        popup2 = BottomPopup,
        buttonClose = function() { return new Button('Close Button') }
    ) {
        super();
        inject(this, name, popup1, popup2, buttonClose);
    }
}

const panel = new Panel('Panel 1');

. . . , inject, .


inject :


  1. (this)
  2. β€” context.constructor.
  3. .
  4. , β€” inject.depsRegistry
  5. β€” context

β€” . β€” Inject , . , .


:


class Injectable {
    constructor(...dependensies) {

       const 
            depsRegistry = Injectable.depsRegistry || (Injectable.depsRegistry = {}),
            className = this.constructor.name;

       let depNames = depsRegistry[className];

       if (!depNames) {
           depNames = this.constructor
                            .toString()
                            .replace(/\s|['"]/g, '')
                            .replace(/.*(super\((?:\w+,?)+\)).*/g, '$1')
                            .replace(/super\((.*)\)/, '$1')
                            .split(',');
       }

       dependensies.forEach((dependense, index) => {
          const depName = depNames[index];
          try {
            this[depName] = new dependense();
          } catch (err) {
            this[depName] = dependense;
          }
       })                     
    }
}

class Component extends Injectable {
    showName() {
        alert(this.name);
    }
}

class Button extends Component {
    constructor(name = 'button') {
        super(name);
    }

    disable() {
        alert(`button ${this.name} is disabled`);
    }

    enable() {
        alert(`button ${this.name} is enabled`);
    }
}

class PopupComponent extends Component {
    show() {
        alert(`show ${this.name} popup`);
    }

    hide() {
         alert(`hide ${this.name} popup`);
    }
}

class TopPopup extends PopupComponent {
    constructor(
        popupButton = Button,
        name = 'Top Popup'
    ) {
        super(popupButton, name); 
        this.popupButton.name = 'TopPopup Button';
    }
}

class BottomPopup extends PopupComponent {
    constructor(
        popupButton = function() { return new Button('BottomPopup Button') },
        name = 'Bottom Popup'
    ) {
        super(popupButton, name);
    }
}

class Panel extends Component {
    constructor(
        name = 'Panel',
        popup1 = TopPopup,
        popup2 = BottomPopup,
        buttonClose = function() { return new Button('Close Button') }
    ) {
        super(name, popup1, popup2, buttonClose);
    }
}

const panel = new Panel('Panel 1');

Injectable. super , .


:



In my opinion, the last - the 3rd option is most suitable for DI & IoC definitions, so that the implementation mechanism is most hidden from the client code.
Well, that’s all. I hope it was interesting and informative. Bye everyone!


All Articles