Four kinds of dependency injection _javascript techniques in JavaScript technology stack

Source: Internet
Author: User
Tags function definition reflection

One of the most common technical tools for implementing control reversal in object-oriented programming (inversion, hereinafter called IOC) is that dependency injection (Dependency injection, hereinafter called DI) is a great way to go in OOP programming. For example, in the Java EE, there is the famous leading spring. There are some positive attempts in the JavaScript community, and the well-known Angularjs are largely based on di. Unfortunately, as a dynamic language that lacks a reflective mechanism and does not support annotation syntax, JavaScript has long had no spring framework of its own. Of course, with the ECMAScript of the draft into a rapid iteration of the spring breeze, the JavaScript community of various dialects, frames are the warlords and rise, in the ascendant. It can be foreseen that the emergence of the excellent JAVASCRIPTDI framework is only a matter of morning and evening.

This article summarizes the common dependency injection methods in JavaScript, and takes Inversify.js as an example, the article is divided into four sections:

I. Dependency injection based on injector, cache, and function parameter names
Two. Angularjs based on double injector Dependency Injection
Three. Typescript dependency injection based on adorner and reflection
Four. IOC container in the Inversify.js--javascript technology stack

I. Dependency injection based on injector, cache, and function parameter names

Although there is no native support for reflection (Reflection) syntax in JavaScript, But the ToString method on the Function.prototype makes it possible for us to pry into the internal construction of a function at run time: The ToString method returns the entire function definition, including the functions keyword, as a string. From this complete function definition, we can use the regular expression to extract the parameters required by the function, and to some extent, we know the operation dependence of the function.
The function signature Write (notebook, pencil) of the Write method on the student class indicates that its execution relies on notebook and pencil objects. Therefore, we can first store the notebook and pencil objects in a cache, and then through the injector (injector, syringe) to the Write method to provide the dependency it needs:

var cache = {}; The parameter name function Getparamnames (func) {var paramnames = func.tostring () is obtained by parsing Function.prototype.toString (). Match (/^
 Function\s*[^\ (]*\ (\s* ([^\)]*) \)/m) [1];
 Paramnames = paramnames.replace (//g, ');
 Paramnames = Paramnames.split (', ');
return paramnames; var injector = {//binds the This keyword in the func scope to a Bind object, which can be empty resolve:function (func, bind) {///get parameter name var paramname
  s = Getparamnames (func);
  var params = [];
  for (var i = 0; i < paramnames.length; i++) {//Remove the corresponding dependent Params.push (Cache[paramnames[i]) in the cache by the parameter name;
 }//injection dependent and execute function func.apply (bind, params);
 
}
};
 
function notebook () {} Notebook.prototype.printName = function () {Console.log (' is a notebook ');
 
function Pencil () {} Pencil.prototype.printName = function () {Console.log (' is a Pencil '); function Student () {} Student.prototype.write = function (notebook, pencil) {if (!notebook | |!pencil) {throw new ERR
 or (' Dependencies not provided! '); } console.log (' WritinG.. ');
};
Provide notebook dependent cache[' notebook ' = new Notebook ();
Provide pencil dependent cache[' pencil ' = new Pencil ();
var student = new Student (); Injector.resolve (student.write, student);
 Writing ...

Sometimes in order to ensure good encapsulation, it is not necessary to expose the cache object to the external scope, more often in the form of closures or private attributes:

function Injector () {this._cache = {};}
 
Injector.prototype.put = function (name, obj) {this._cache[name] = obj;}; Injector.prototype.getParamNames = function (func) {var paramnames = func.tostring (). Match (/^function\s*[^\ (]*\ (\s*) ([
 ^\) (]*) ()/m) [1];
 Paramnames = paramnames.replace (//g, ');
 Paramnames = Paramnames.split (', ');
return paramnames;
 
};
 Injector.prototype.resolve = function (func, bind) {var self = this;
 var paramnames = Self.getparamnames (func);
 var params = Paramnames.map (function (name) {return self._cache[name];
 });
Func.apply (bind, params);
 
};
 
var injector = new Injector ();
var student = new Student ();
Injector.put (' notebook ', New Notebook ()); Injector.put (' Pencil ', New Pencil ()) Injector.resolve (student.write, student);
Writing ... For example, now perform another method on the student class, function draw (notebook, pencil, eraser), because injector and notebook objects are already in the cache of pencil. We just need to store the extra Eraser in the cache: function Eraser () {} Eraser.prototype.printName = function () {Console.log (' this is a eraser ');
 
}; 
  Add Draw method for Student Student.prototype.draw = function (notebook, pencil, eraser) {if (!notebook | |!pencil | |!eraser) {
 throw new Error (' Dependencies not provided! ');
} console.log (' Drawing ... ');
 
};
Injector.put (' Eraser ', New Eraser ());
 Injector.resolve (Student.draw, student);

By relying on injection, the execution of functions and the creation logic of the objects on which they are dependent are decoupled.
Of course, with the popularization of Grunt/gulp/fis and other front-end engineering tools, more and more projects in the line before the code confusion (uglify), so through the parameter name to judge dependence is not always reliable, It is also sometimes a clear indication of its dependencies by adding additional attributes to the function:

Student.prototype.write.depends = [' notebook ', ' pencil '];
Student.prototype.draw.depends = [' notebook ', ' pencil ', ' eraser '];
Injector.prototype.resolve = function (func, bind) {
 var self = this;
 First check whether there are depends properties on the Func, if not, then use regular expressions to parse
 func.depends = Func.depends | | self.getparamnames (func);
 var params = Func.depends.map (function (name) {return
  self._cache[name];
 });
 Func.apply (bind, params);
var student = new Student ();
Injector.resolve (student.write, student);
writing ... Injector.resolve (Student.draw, student); Draw ...
 

Two. Angularjs based on double injector Dependency Injection

Students who are familiar with ANGULARJS will soon be able to recall that, before injector injection, we can also call the Config method to configure the objects that will then be injected when the module is defined. A typical example is the configuration of $routeprovider when using routing. That is, unlike in the previous section where ready-made objects (such as New Notebook ()) are stored directly in cache, dependency injection in ANGULARJS should also have a process of "instantiating" or "invoking factory methods."
This is the origin of Providerinjector, Instanceinjector and their respective providercache and Instancecache.
In Angularjs, the injector that we can get through dependency injection is usually instanceinjector, while Providerinjector is in the form of a variable in the closure. Whenever we need angularjs to provide dependency injection services, such as trying to get notebook,instanceinjector, we will first query the notebook attribute on the Instancecache, if it exists, direct injection; Then handing this task to Providerinjector;providerinjector will stitch the "Provider" string to the back of the "notebook" string, forming a new key name "Notebookprovider", Again to Providercache query whether there is notebookprovider this attribute, if there is no, then throw an exception unknown provider exception:

If there is, then return this provider to Instanceinjector;instanceinjector to get Notebookprovider, will call the factory method on Notebookprovider $get, Gets the return value notebook object, puts the object in the Instancecache for future use, and injects it into a function that initially declares the dependency. The process is more complex to describe, and can be illustrated by the following illustration:

Note that the dependency injection approach in ANGULARJS is also flawed: the global side effect of using a single instanceinjector service is that it is not possible to track and control a dependent chain alone, even without cross dependencies Provider with the same name in different module will also produce overlays, which are not expanded in detail here.

In addition, for students accustomed to high-level IOC containers in languages such as Java and C #, it may seem awkward to see here, after all, in oop, we usually do not pass dependencies as arguments to the method, but rather as properties passed to the instance by constructor or setters, To implement encapsulation. Indeed, the dependency injection approach in section one or two does not embody enough object-oriented features, which have existed in JavaScript for many years and do not even require ES5 syntax support. Students who want to learn about the research and results of dependency injection over the last year or two in the JavaScript community can continue reading.

three. Typescript Dependency injection based on adorner and reflection

The author itself is not particularly enthusiastic about the various dialects of JavaScript, especially now Emcascript proposals, drafts updated quickly, many times with the help of Polyfill and Babel preset can meet the needs. But Typescript is an exception (of course now decorator is already a proposal, although the stage is relatively early, but indeed there are polyfill can be used). As mentioned above, there is a lingering lack of a good IOC container in the JavaScript community and its own language features, so what difference does typescript bring to us on the subject of injection? There are at least the following points:
* typescript adds compile-time type checking to give JavaScript a certain static language feature
* typescript support Adorner (decorator) syntax, Similar to traditional annotations (Annotation)
* typescript support meta information (METADATA) reflection, no longer need to invoke Function.prototype.toString method
Here we try to standardize and simplify dependency injection with the new syntax brought by typescript. This time we no longer inject dependency into a function or method, but inject it into the constructor of the class. The
Typescript supports decorating classes, methods, properties, and function parameters, where you need to decorate the class. Continue with the example in the above section, using Typescript to refactor the code:

 class Pencil {public Printname () {Console.log (' a Pencil ');
 } class Eraser {public Printname () {console.log (' the ' is a Eraser ');
 } class Notebook {public Printname () {Console.log (' a notebook ');
 } class Student {pencil:pencil;
 Eraser:eraser;
 Notebook:notebook;
  Public constructor (Notebook:notebook, Pencil:pencil, eraser:eraser) {This.notebook = notebook;
  This.pencil = pencil;
 This.eraser = eraser;
  Public write () {if (!this.notebook | |!this.pencil) {throw new Error (' Dependencies not provided! ');
 } console.log (' writing ... '); Public Draw () {if (!this.notebook | |!this.pencil | |!this.eraser) {throw new Error (' Dependencies not provided! '
  );
 } console.log (' Drawing ... '); }
}

The following is the implementation of the injector and adorner inject. Injector's Resolve method, when it receives an incoming constructor, takes the name of the constructor from the Name property, such as Class Student, whose Name property is the string "Student". Then the student as key, to Dependenciesmap to remove the student dependency, as to Dependenciesmap when the dependency, which is the adorner inject logic, will be discussed later. Student dependencies are taken out, because these dependencies are already references to constructors rather than simple strings (such as notebook, pencil constructors), so you can get those objects directly using the new statement. After you get to the objects that the student class relies on, how do you pass these dependencies into the student as parameters for the constructor? The simplest is ES6 's spread operator. In an environment where ES6 cannot be used, we can also complete the above logic by forging a constructor function. Note In order for the instanceof operator to not fail, the prototype property of this bogus constructor should point to the prototype property of the original constructor.

var dependenciesmap = {};
var injector = {
 Resolve:function (constructor) {
  var dependencies = Dependenciesmap[constructor.name];
  dependencies = Dependencies.map (function (dependency) {return
   new dependency ();
  });
  If you can use ES6 syntax, the following code can be merged into one line:
  //return new constructor (... dependencies);
  var mockconstructor:any = function () {
   constructor.apply (this, dependencies);
  Mockconstructor.prototype = Constructor.prototype;
  return new Mockconstructor ();
 }
;
function inject (... dependencies) {return
 function (constructor) {
  Dependenciesmap[constructor.name] = dependencies;
  return constructor;}

After the logic of the injector and adorner inject is complete, you can use it to decorate class student and enjoy the pleasure of dependency injection:

The use of the
 

Using adorners, we can also implement a more radical dependency injection, hereinafter referred to as Radicalinject. Radicalinject to the original code intrusion is relatively strong, not necessarily suitable for specific business, here also introduced. To understand radicalinject, it is necessary to understand the principle of the typescript adorner and the reduce method on the Array.prototype.

function Radicalinject (... dependencies) {
 var wrappedfunc:any = function (target:any) {
  dependencies = Dependencies.map (function (dependency) {return
   new dependency ();
  });
  The reason for using Mockconstructor is the same
  function Mockconstructor () {
   target.apply (this, dependencies) as the previous example;
  Mockconstructor.prototype = Target.prototype;
 
  Why do you need to use Reservedconstructor? Since the student method was decorated with radicalinject, the
  //Student Point constructor is not the class student we declared at the beginning, but the return value here.
  namely Reservedconstructor. Student's point of change is not an acceptable thing to do, but if you want to/
  guarantee student instanceof student work as we expect, it should be
  The Reservedconstructor prototype attribute points to the prototype function of the original student
  Reservedconstructor () {return
   new Mockconstructor ();
  }
  Reservedconstructor.prototype = Target.prototype;
  return reservedconstructor;
 }
 return wrappedfunc;
}

Using Radicalinject, the original constructor has essentially been represented by a new function, and is simpler to use, and does not even require a injector implementation:

 @RadicalInject (notebook, Pencil, Eraser) class Student {pencil:pencil;
 Eraser:eraser;
 Notebook:notebook; Public constructor () {} public constructor (Notebook:notebook, Pencil:pencil, eraser:eraser) {This.notebook = Notebo
  Ok
  This.pencil = pencil;
 This.eraser = eraser;
  Public write () {if (!this.notebook | |!this.pencil) {throw new Error (' Dependencies not provided! ');
 } console.log (' writing ... '); Public Draw () {if (!this.notebook | |!this.pencil | |!this.eraser) {throw new Error (' Dependencies not provided! '
  );
 } console.log (' Drawing ... ');
}//no longer appears injector, direct call constructor var student = new Student (); Console.log (student instanceof Student); True Student.notebook.printName (); This is a notebook student.pencil.printName (); This is a pencil student.eraser.printName (); This is a eraser student.draw (); Drawing Student.write (); Writing 

Because the class Student constructor method needs to receive three parameters, a direct parameterless call to New Student () can cause the typescript compiler to make an error. Of course, this is just a way of sharing ideas, you can temporarily ignore this error. Interested students can also use similar ideas to try to proxy a factory method instead of a direct proxy constructor to avoid such errors, which are not expanded here.

The ANGULARJS2 team was once ready to start a new framework based on Atscript (atscript "A" refers to annotation) in order to obtain better adorner and reflective grammar support. But finally chose to embrace typescript, so there is a wonderful combination of Microsoft and Google.

Of course, it is important to note that, in the absence of relevant standards and browser vendor support, typescript is only pure JavaScript at run time, as the example in the following section confirms.

Four. IOC container in the Inversify.js--javascript technology stack

In fact, the emergence of various languages that support advanced language features from JavaScript can be foreseen, the emergence of the IOC container is only a matter of morning and evening. For example, the blogger today to introduce the typescript based on the Inversify.js, is one of the forerunners.
Inversity.js is much more advanced than the example of the blogger in the previous section, and it was originally designed to be a very high goal for the front-end engineers to write code that conforms to the solid principle in JavaScript. In the code, there is the interface everywhere, will "Depend upon abstractions." Do not depend upon concretions. " (dependent on abstraction, not on concrete) performing incisively and vividly. Continue using the example above, but since Inversity.js is interface-oriented, the above code needs to be further refactored:

Interface Notebookinterface {printname (): void; interface Pencilinterface {printname (): void;} interface Eraserint
 Erface {printname (): void;} interface Studentinterface {notebook:notebookinterface;
 Pencil:pencilinterface;
 Eraser:eraserinterface;
 Write (): void;
Draw (): void;
 Class Notebook implements Notebookinterface {public printname () {Console.log (' a notebook ');
 The class Pencil implements Pencilinterface {public printname () {Console.log (' a Pencil ');
 Class Eraser implements Eraserinterface {public printname () {console.log (' the ' is a Eraser ');
 } class Student implements Studentinterface {notebook:notebookinterface;
 Pencil:pencilinterface;
 Eraser:eraserinterface;
  Constructor (Notebook:notebookinterface, Pencil:pencilinterface, eraser:eraserinterface) {This.notebook = notebook;
  This.pencil = pencil;
 This.eraser = eraser;
 Write () {console.log (' writing ... ');
 } draw () {Console.log (' drawing ... ');}
}
 

With the inversity framework, we don't have to implement the injector and inject adorners ourselves, we just need to refer to the relevant objects from the Inversify module:

Import {inject} from "inversify";
 
@Inject ("Notebookinterface", "Pencilinterface", "Eraserinterface")
class Student implements Studentinterface {
 Notebook:notebookinterface;
 Pencil:pencilinterface;
 Eraser:eraserinterface;
 Constructor (Notebook:notebookinterface, Pencil:pencilinterface, eraser:eraserinterface) {
  This.notebook = notebook;
  This.pencil = pencil;
  This.eraser = eraser;
 }
 Write () {
  console.log (' writing ... ');
 }
 Draw () {
  console.log (' drawing ... ');
 }
}

Is that all you got? Remember that in the last section, the concepts of typescript are just grammatical sugars? Unlike the example in the previous section where the constructor reference is passed directly to inject, the inversify.js is interface-oriented, and such as Notebookinterface, Interfaces such as pencilinterface are only syntactic sugars provided by typescript and do not exist at run time, so we can only use string form instead of reference form when declaring dependencies in adorners. But don't worry, Inversify.js provides us with a bind mechanism that bridges the string form of an interface with a specific constructor:

Import {typebinding, Kernel} from "Inversify";
 
var kernel = new kernel ();
Kernel.bind (New typebinding<notebookinterface> ("Notebookinterface", Notebook));
Kernel.bind (New typebinding<pencilinterface> ("Pencilinterface", Pencil));
Kernel.bind (New typebinding<eraserinterface> ("Eraserinterface", Eraser));
Kernel.bind (New typebinding<studentinterface> ("Studentinterface", Student));

Note that this step requires the introduction of typebinding and kernel from the Inversify module, and the generic syntax is used to ensure that the return value type and the entire compile-time static type check pass smoothly.
It is natural to understand the new typebinding<notebookinterface> ("Notebookinterface", Notebook): To rely on "notebookinterface" The class of the string provides an instance of the notebook class, returning the value to the trace type to the Notebookinterface.
These steps have been completed, and they are also handy to use:

var student:studentinterface = kernel.resolve<studentinterface> ("Studentinterface");
Console.log (student instanceof Student); True
student.notebook.printName ();//This are a notebook
student.pencil.printName ();//This is a pencil
s Tudent.eraser.printName (); This is a eraser
student.draw ();//Drawing
student.write ();//Writing

The above is about the JavaScript technology stack in the four kinds of dependency injection of all the content, I hope to help you learn.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.