📜 ⬆️ ⬇️

Auto dependency injection in javascript

Introduction


As we all know, javascript is a language in which it is very easy to shoot yourself in the foot. Working with this language for almost five years, I have often come across the fact that javascript provides very scant tools for creating high-level abstractions. And when creating full-fledged MVVM / MVP applications, you encounter the fact that the main problem is the difficulty of keeping the code and abstraction clean, not to mention fully following 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 it in JS.
Of course, JS does not provide tools to fully follow this pattern (the elementary absence of the same reflections), so I decided to put on 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. (except for common libraries)
2. The ability to upgrade or change 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 for 100%

After several days of thinking about how I want to see 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 application creation approach. As a result, I came to a small manager, providing access to all components of the application, as well as being able to assemble components using the JSON configuration.
')
Attention please. Immediately prudpredzhuyu - that this is not a classic Dependency Injection pattern, but very adapted to the JS environment and to my needs, so do not send me with kicks to read the specification. Criticism will be very happy.

Examples of using


Case 1

The GreeterClass class, which welcomes the user, the greeting method and text is 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 the class dependencies:
 SERVICES['dependency']['greet-class'] = { 'greetMethod' : {'object' : 'alert'}, 'textMsg' : {'value' : 'Hello world'} }; 

We request an instance of the 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 code organization, but I think it’s worth explaining what happened here. After the call:
 DI.get('greet-class').say(); 

The following processes occur in DI:
1. Look for 'greet-class' in the list of services, after it is instantiated.
2. Dependencies are loaded.
3. There is a check - whether there are methods in 'greet-class' with the same name as the dependencies.
4. If such methods are not observed - they are created, with the name coinciding with the name of the dependency and a kind of prefix _get. Such a method returns an injected dependency when called.
5. If such methods exist, they are called, and the dependency is passed as an argument.

That is, the methods ._getGreetMethod () and. _getTextMsg () is art and is 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 us face the challenge of changing 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 wanted.

Case 3

Now a simple object is injected into greetMethod, but it can also be another service with its dependencies.
DI also has several other responsibilities. For example, it may be something like a "multion"

Example:
 SERVICES['config']['greet-class'] = { 'singleton' : true } DI.get('greet-class') === DI.get('greet-class'); // true 


Case 4

Substitution dependencies find:
 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 concept of DI (sometimes necessary);
 SERVICES['dependency']['greet-class'] = { 'greetMethod' : {'value' : function(txt){document.body.innerHTML = txt}}, 'textMsg' : {'value' : 'Hello world'} }; DI.get('greet-class').say(); 


Result:


Total


And this is what my DI config looks like for a test application:
/ * not yet without hacks * /
 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 nestings and zavimost? Well, imagine how to understand all this when even there is no such card.
In my opinion it is very convenient, the map of the entire application is immediately visible, it is possible to take it all in, and most importantly, this approach makes you write the correct code.

Another very important point is that this config may be generated on the server and vary from different parameters, for example, for privileged users the config may differ from the standard one, as a result it 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.

The DI code on GIThub I must say that many moments “may be easier”, but at the moment I am working on applications for Samsung SmartTV, so it is “adapted” in some places. He also tried to adhere to the KISS principle. Naturally, if DI justifies itself, I will add two drivers for reading the config with JSON and XML.

The demo application about which was written above - wrote directly under webkit, in other browsers did not test. Alas.

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

* Case 1 updated

Source: https://habr.com/ru/post/149912/


All Articles