Auto dependency injection in javascript

Introduction


As we all know, javascript is a language in which it’s very easy to shoot yourself in the foot. Having been working with this language for almost five years, I have often come across the fact that javascript provides very meager tools for creating high-level abstractions. And when creating full-fledged MVVM / MVP applications, you come across the fact that the main problem is the difficulty to keep the code and abstraction clean, not to mention the full implementation of SOLID principles.

Over time, I came to understand that one of the main patterns that could help me is Dependency Injection . And I decided to experiment with him in JS.
Of course, JS does not provide tools for fully following this pattern (the elementary absence of the same reflexes), so I decided to set for myself several Acceptance Criteria, which I would like to achieve by adapting this pattern to such a unique environment as JS.

1. Get rid of all possible global variables. (with the exception of common libraries)
2. The ability to upgrade or modify the behavior of the application without changing its code.
3. Have a complete dependency map.
4. Remove all "implicit" in the structure of the application.
5. Make a code that can be covered with tests 100%

After several days of thinking about how I want to see the DI manager, I wrote it literally in one evening. Then, on the weekend, I wrote a small application (WYSIWYG template editor) to look at the bottlenecks in this approach to creating applications. As a result, I came to a small manager that provides access to all components of the application, as well as being able to assemble components using the JSON config.

Attention please. Immediately I warn you - that this is not a classic Dependency Injection pattern, but very adapted for the JS environment and for my needs, so I don’t need to send me kicks to read the specification. I will be very glad to criticism.

Examples of using


Case 1

The GreeterClass class that greets the user, the method, and the welcome text are set by injection:
var GreeterClass = function(){
    this.say = function(){
        var method = this._getGreetMethod(); 
        var greet = this._getTextMsg();
        method(greet);
    };
};
SERVICES['constructor']['greet-class'] = GreeterClass; //записываем класс в пул сервисов доступных DI

We describe class dependencies:
SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'object' : 'alert'},
    'textMsg' : {'value' : 'Hello world'}
};

We query the instance GreeterClass class and call the say method:
DI.get('greet-class').say();

Result:


UPD

This article is not about code, but about the approach to organizing code, but I think it’s worth explaining what happened here. After the call:
DI.get('greet-class').say();

In DI, the following processes occur:
1. Look for a 'greet-class' in the list of services, after which it is instantiated.
2. Dependencies are loaded.
3. A check is underway to see if there are methods in the 'greet-class' with a name matching the dependency names.
4. If such methods are not observed, they are created with a name that matches the name of the dependency and a kind of prefix _get. When called, this method returns the injected dependency.
5. If such methods exist, they are called, and the dependency is passed as an argument.

That is, the ._getGreetMethod () and. _getTextMsg () and artificial are created dynamically in the DI manager.
To make it clearer, I made an example with a predefined method:
SERVICES['constructor']['stack'] = function(){
	var stack = [];
	
	this.flush = function(){
		console.log(stack);
	};
	
	this.push = function(el){
		/*** some actions ***/
		stack.push(el);
		return this;
	};
}

SERVICES['dependency']['stack'] = {
	'push' : [
		{'value' : 1},
		{'value' : 2},
		{'value' : 3}
	]
};
DI.get('stack').flush(); // [1,2,3]

Here DI called the native push method for each dependency.

Case 2

Let's say we have a task to change the output method:
SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'object' : 'console.log'},
    'textMsg' : {'value' : 'Hello world'}
};


Result:
I changed the implementation without changing the abstraction, which is what I achieved.

Case 3

Now a simple object is injected into greetMethod, but it could also be another service with its dependencies.
DI also has several other responsibilities. For example, it can be something like a "multion"
Example:
SERVICES['config']['greet-class'] = {
    'singleton' : true
}
DI.get('greet-class') === DI.get('greet-class'); // true


Case 4

I find the substitution of dependencies:
DI.get('greet-class').say(); // Hello world
DI.get('greet-class', {'textMsg' : {'value' : 'Bye world'}}).say(); //Bye world


Case 5

The ability to create "hacks" that do not fit into the DI concept (sometimes necessary);
SERVICES['dependency']['greet-class'] = {
    'greetMethod' : {'value' : function(txt){document.body.innerHTML = txt}},
    'textMsg' : {'value' : 'Hello world'}
};
DI.get('greet-class').say(); 


Result:


Summary


And here is my DI config for a test application:
/ * not without hacks so far * /
DEPENDENCY['application'] = {
    'template-manager' : {
        'addWidgetModel' : [
            {
                'service' : 'widget-model',
                'dependency' : { 'domainObject' : {'instance' : function(){return WidgetDO(incomingWidget);}}} /*TODO: remove this hack*/
            },
            {
                'service' : 'widget-model',
                'dependency' : { 'domainObject' : {'instance' : function(){return WidgetDO(incomingWidget2);}}} /*TODO: remove this hack*/
            }
        ],
        'toolsManager' : {
            'service' : 'widget-manager',
            'dependency' :{
                'addRenderer' : {
                    'service' : 'text-tools-renderer',
                    'dependency' : {
                        'richView' : {
                            'service-constructor' : 'rich-view',
                            'dependency': {
                                'setEventManager' : {
                                    'service' : 'event-manager',
                                    'dependency' : {
                                        'setContext' : {'poll' : 'rich-view'}
                                    }
                                },
                                'template' : {'value' : 'code/template/tools.html'}
                            }
                        }
                    }
                },
                'addHandler' : {'instance' : 'TextToolsHandler'},
                'containerRenderer' : {
                    'service' : 'rich-view',
                    'dependency': {
                        'setEventManager' : {
                            'service' : 'event-manager',
                            'dependency' : {
                                'setContext' : {'poll' : 'rich-view'}
                            }
                        },
                        'template' : {'value' : 'code/template/tools-container.html'}
                    }
                }
            }
        },
        'editorManager' : {
            'service' : 'widget-manager',
            'dependency' :{
                'addRenderer' : {
                    'service' : 'text-editor-renderer',
                    'dependency' : {
                        'globalEventManager' : {'service' : 'global-event-manager'},
                        'richView' : {
                            'service-constructor' : 'rich-view',
                            'dependency': {
                                'setEventManager' : {
                                    'service' : 'event-manager',
                                    'dependency' : {
                                        'setContext' : {'poll' : 'rich-view'}
                                    }
                                },
                                'template' : {'value' : 'code/template/editor.html'}
                            }
                        }
                    }
                },
                'addHandler' : {'instance' : 'TextEditorHandler'},
                'containerRenderer' : {
                    'service' : 'rich-view',
                    'dependency': {
                        'setEventManager' : {
                            'service' : 'event-manager',
                            'dependency' : {
                                'setContext' : {'poll' : 'rich-view'}
                            }
                        },
                        'template' : {'value' : 'code/template/editor-container.html'}
                    }
                }
            }
        },
        'applicationRenderer' : {
            'service' : 'rich-view',
            'dependency': {
                'setEventManager' : {
                    'service' : 'event-manager',
                    'dependency' : {
                        'setContext' : {'poll' : 'rich-view'}
                    }
                },
                'template' : {'value' : 'code/template/application.html'}
            }}
    },
    'widget-manager' : {},
    'widget-model' : {
        'eventManager' : {
            'service' : 'event-manager',
            'dependency' : {
                'setContext' : {'poll' : 'widget-model'}
            }
        }
    },
    'global-event-manager' : {
        'context' : {'object' : 'window'}
    }
};
SERVICES['config'] = {
    'global-event-manager' : {
        'singleton' : true
    }
};

Wow, so many nesting and currencies? Well, imagine how to understand all this when even such a card is not.
In my opinion, it’s very convenient, you can immediately see the map of the entire application, it’s possible to lock everything, and most importantly - this approach makes you write the correct code.

Another very important point is that this config can be generated on the server and vary from various parameters, for example, for privileged users, the config may differ from the standard one, as a result, he will see another application.

I believe that this approach justifies itself, but I would like to hear objective criticism of both the approach and the manager himself.

DI Code on GIThub I must say that many points “could be easier”, but at the moment I’m working on applications for Samsung SmartTV, so it’s “adapted” in some places. I also tried to adhere to the KISS principle. Naturally, if the DI justifies itself, I will add two drivers to read the config with JSON and XML.

The demo application described above was written directly under webkit, in other browsers it was not tested. Alas.

PS: I already use this approach at work, I'm happy as an elephant. For complete happiness, it remains only to connect some kind of contract manager.

* Case 1 updated