Four dependency injection methods in the Javascript technology stack _ javascript skills

Source: Internet
Author: User
This article mainly introduces four types of dependency injection in the Javascript technology stack. If you are interested, refer to the Inversion of Control in object-oriented programming, (IoC) One of the most common technical means, Dependency Injection (DI) is widely used in OOP programming. For example, in J2EE, Spring is a well-known master. There are naturally some positive attempts in the Javascript community. the widely known AngularJS is implemented based on DI to a large extent. Unfortunately, as a dynamic language that lacks the reflection mechanism and does not support the Annotation syntax, Javascript has never belonged to its own Spring framework for a long time. Of course, as the draft ECMAScript enters the fast iteration period, various dialects and frameworks in the Javascript community are in the ascendant. It is foreseeable that the appearance of an excellent JavascriptDI framework is only a matter of time.

This article summarizes common dependency injection methods in Javascript, and uses inversify. js as an example. The article is divided into four sections:

1. Dependency Injection Based on Injector, Cache, and function parameter names
2. Dependency Injection Based on Dual Injector in AngularJS
Iii. Dependency Injection Based on decorator and reflection in TypeScript
Iv. inversify. js -- IoC container in the Javascript technology Stack

1. Dependency Injection Based on Injector, Cache, and function parameter names

Although Javascript does not support the Reflection syntax, Function. the toString Method on prototype has a different path for us, which makes it possible to snoop into the internal structure of a function during runtime: The toString method returns the entire function definition including the function keyword in the form of a string. Starting from the complete function definition, we can use regular expressions to extract the parameters required by the function, so as to know the running dependencies of the function to some extent.
For example, the function signature of the write method in the Student class write (notebook, penpencil) indicates that its execution depends on the notebook and penpencil objects. Therefore, we can first store the notebook and pencil objects in a cache, and then provide the necessary dependencies to the write method through injector (injector and injector:

Var cache ={}; // parses the Function. prototype. toString () gets the parameter name function getParamNames (func) {var paramNames = func. toString (). match (/^ function \ s * [^ \ (] * \ (\ s * ([^ \)] *) \)/m) [1]; paramNames = paramNames. replace (// g, ''); paramNames = paramNames. split (','); return paramNames;} var injector = {// bind the this keyword in the func scope to the bind object. The bind object can be empty. resolve: function (func, bind) {// get the parameter name var paramNames = getParamNames (fun C); var params = []; for (var I = 0; I <paramNames. length; I ++) {// retrieve the corresponding dependent params in the cache using the parameter name. push (cache [paramNames [I]);} // inject dependencies and execute the function func. apply (bind, params) ;}}; function Notebook () {} Notebook. prototype. printName = function () {console. log ('this is a NOTEBOOK');}; function penpencil () {} penpencil. prototype. printName = function () {console. log ('this is a pension');}; function Student () {} Student. Prototype. write = function (notebook, penpencil) {if (! Notebook |! Penpencil) {throw new Error ('dependencies not provided! ');} Console. log ('writing... ') ;}; // provides the notebook dependency cache ['notebook'] = new notebook (); // provides the penpencil dependency cache ['pencil'] = new penpencil (); var student = new Student (); injector. resolve (student. write, student); // writing...

Sometimes, to ensure good encapsulation, you do not have to expose the cache object to external scopes. More often, they exist in the form of closure variables 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 penpencil () injector. resolve (student. write, student); // writing... for example, you need to execute another function draw (noteboo K, pencer, eraser), because the injector cache already has notebook and pencer objects, we only need to store the extra eraser in the cache: function Eraser () {} Eraser. prototype. printName = function () {console. log ('this is an eraser ');}; // Add the draw method Student to Student. prototype. draw = function (notebook, penpencil, eraser) {if (! Notebook |! Penpencil |! Eraser) {throw new Error ('dependencies not provided! ');} Console. log ('Drawing... ') ;}; injector. put ('eraser', new eraser (); injector. resolve (student. draw, student );

Through dependency injection, the execution of functions is decoupled from the creation logic of the dependent objects.
Of course, with the popularization of front-end engineering tools such as grunt, gulp, and FIPS, more and more projects have undergone uglify before going online ), therefore, it is not always reliable to judge the dependency by parameter name. Sometimes, the dependency is explicitly described 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 the depends attribute exists on func. If not, use a regular expression 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...

2. Dependency Injection Based on Dual Injector in AngularJS

Those familiar with AngularJS will soon be able to think that before injector injection, we can also call the config method when defining the module to configure the objects to be injected subsequently. A typical example is the configuration of $ routeProvider when routing is used. That is to say, unlike the method of directly storing existing objects (such as new Notebook () into the cache in the previous section, dependency injection in AngularJS should also have a process of "instantiation" or "calling factory methods.
This is the origin of providerInjector, instanceInjector, and their respective providerCache and instanceCache.
In AngularJS, the injector we can obtain through dependency injection is usually instanceInjector, while providerInjector exists in the form of variables in the closure. Whenever AngularJS is required to provide the dependency injection service, for example, to obtain the notebook, instanceInjector first queries whether the notebook attribute exists on instanceCache. If yes, injection is performed directly. If no, the task is handed over to providerInjector. providerInjector Concatenates the "Provider" string to the end of the "notebook" string to form a new key name "notebookProvider ", go to providerCache to check whether the notebookProvider attribute exists. If yes, an Unknown Provider exception is thrown:

If yes, the provider is returned to instanceInjector. After getting the notebookProvider, instanceInjector calls the factory method $ get On The notebookProvider to obtain the notebook object returned value, put this object in instanceCache for future use, and inject it into the function that declares this dependency at the beginning. The process description is complex and can be illustrated as follows:

It should be noted that the dependency injection method in AngularJS is also flawed: the global side effect of using an instanceInjector Singleton service is that a dependency chain cannot be tracked and controlled independently, even if there is no cross-dependence, providers with the same name in different modules will overwrite them, which will not be detailed here.

In addition, for those who are used to Java, C #, and other languages, they may feel awkward here. After all, in OOP, we usually do not pass dependencies to methods in the form of parameters, but pass them to instances as attributes through constructor or setters for encapsulation. Indeed, the dependency injection method in sections 1 and 2 does not show enough object-oriented features. After all, this method has been in Javascript for many years and does not even require ES5 syntax support. If you want to learn about the research and achievements of dependency injection in the Javascript community in the past one or two years, you can continue to read it.

Iii. Dependency Injection Based on decorator and reflection in TypeScript

I am not very enthusiastic about the learning of various dialects of Javascript, especially the current e-mapreduce proposals and drafts are updated very quickly. Many times, with the help of various presets of polyfill and babel, I can meet my needs. However, TypeScript is an exception (of course, Decorator is also a proposal now. Although the stage is still relatively early, polyfill is already available ). As mentioned above, the Javascript community has never seen a good IoC container related to its own language features. In terms of dependency injection, what difference does TypeScript bring to us? There are at least the following points:
* TypeScript adds the compile-time type check, enabling Javascript to have certain static language features.
* TypeScript supports the Decorator syntax, which is quite similar to the traditional Annotation (Annotation ).
* TypeScript supports Metadata reflection and does not need to call Function. prototype. toString.
Next we will try to use the new syntax of TypeScript to standardize and simplify dependency injection. This time, we will not inject dependencies into functions or methods, but to class constructors.
TypeScript supports decoration of classes, methods, attributes, and function parameters. The decoration of classes is used here. Continue with the examples used in the above section and use TypeScript to refactor the Code:

class Pencil { public printName() {  console.log('this is a pencil'); }} class Eraser { public printName() {  console.log('this is an eraser'); }} class Notebook { public printName() {  console.log('this is 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 describes the implementation of injector and Inject. When the resolve Method of injector receives the passed constructor, the name of the constructor is obtained through the name attribute, such as class Student. Its name attribute is the string "Student ". Then, use Student as the key and go to dependenciesMap to retrieve the dependencies of Student. As for the dependencies stored in dependenciesMap, This is the logic of Inject, which will be discussed later. After the dependency of Student is retrieved, because these dependencies are references of constructors rather than simple strings (such as Notebook and Pencil constructors ), therefore, you can directly use the new statement to obtain these objects. After obtaining the objects dependent on the Student class, how can we pass these dependencies into Student as parameters of the constructor? The simplest is ES6's spread operator. In an environment where ES6 cannot be used, we can also forge a constructor to complete the above logic. Note that in order to make the instanceof operator not invalid, the prototype attribute of the forged constructor should point to the prototype attribute 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 row: // 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 completing the logic of injector and Inject, you can describe class Student and enjoy the fun of dependency injection:

// The decorator is easy to use. You only need to add a line of code above the class definition. // Inject is the name of the decorator, followed by the function Inject parameter @ Inject (Notebook, pencer, Eraser) class Student {pencer: pencer; eraser: Eraser; notebook: Notebook; public constructor (notebook: Notebook, pencer: pencer, eraser: Eraser) {this. notebook = notebook; this. penpencil = penpencil; this. eraser = eraser;} public write () {if (! This. notebook |! This. penpencil) {throw new Error ('dependencies not provided! ');} Console. log ('writing...');} public draw () {if (! This. notebook |! This. penpencil |! This. eraser) {throw new Error ('dependencies not provided! ');} Console. log ('Drawing... ') ;}} var student = injector. resolve (Student); console. log (student instanceof Student); // truestudent. notebook. printName (); // this is a notebookstudent. penpencil. printName (); // this is a pencilstudent. eraser. printName (); // this is an eraserstudent. draw (); // drawingstudent. write (); // writing

With the decorator, we can also implement a more radical dependency injection, which is called RadicalInject below. RadicalInject is highly invasive to the original code and may not be suitable for specific services. We will also introduce it here. To understand RadicalInject, we need to understand the principles of the TypeScript decorator and the reduce method on 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 as that in the previous example. function mockConstructor () {target. apply (this, dependencies);} mockConstructor. prototype = target. prototype; // Why does reservedConstructor need to be used? Because after using RadicalInject to decorate the Student method, // The constructor pointed to by Student is not the class Student we declared at the beginning, but the return value here, // is reservedConstructor. Student's point is not unacceptable, but if you want to // ensure that student instanceof Student works as expected, the prototype attribute of // reservedConstructor should be directed to the prototype function reservedConstructor () {return new mockConstructor ();} reservedConstructor. prototype = target. prototype; return reservedConstructor;} return wrappedFunc ;}

Using RadicalInject, the original constructor is essentially replaced by a new function proxy, which is easier to use and does not even require the implementation of injector:

@ RadicalInject (Notebook, pencer, Eraser) class Student {pencer: pencer; eraser: Eraser; notebook: Notebook; public constructor () {} public constructor (notebook: Notebook, penpencil: penpencil, eraser: Eraser) {this. notebook = notebook; this. penpencil = penpencil; this. eraser = eraser;} public write () {if (! This. notebook |! This. penpencil) {throw new Error ('dependencies not provided! ');} Console. log ('writing...');} public draw () {if (! This. notebook |! This. penpencil |! This. eraser) {throw new Error ('dependencies not provided! ');} Console. log ('Drawing... ') ;}} // the injector is no longer displayed. Call the constructor var student = new Student (); console. log (student instanceof Student); // truestudent. notebook. printName (); // this is a notebookstudent. penpencil. printName (); // this is a pencilstudent. eraser. printName (); // this is an eraserstudent. draw (); // drawingstudent. write (); // writing

Because the constructor method of class Student needs to receive three parameters, directly calling new Student () without parameters will cause the TypeScript compiler to report an error. Of course, this is just a way of thinking. You can ignore this error for the moment. If you are interested, you can use a similar idea to try to represent a factory method, rather than a direct proxy constructor, to avoid such errors.

The AngularJS2 team was prepared to develop A new framework based on AtScript ("A" in AtScript refers to Annotation) for better support of the decorator and reflection syntax. But finally they chose to embrace TypeScript, so they had a wonderful combination of Microsoft and Google.

Of course, it should be noted that, in the absence of relevant standards and browser vendor support, TypeScript is only pure Javascript at runtime, and the examples shown in the next section will prove this.

Iv. inversify. js -- IoC container in the Javascript technology Stack

In fact, we can see from the appearance of various dialects supporting advanced language features in Javascript that the emergence of IoC containers is just a matter of time. For example, the TypeScript-based inversify. js introduced by bloggers today is one of the pioneers.
Inversity. js is much more advanced than the example implemented by the blogger in the previous section. It was initially designed to enable front-end engineers to write code that complies with the SOLID principle in Javascript. In the code, there is an interface everywhere, and the "Depend upon indexing actions. Do not depend upon concretions." (dependent on abstraction, rather than on specifics) is presented to the fullest. Continue to use the above example, but since inversity. js is interface-oriented, the above code needs to be further reconstructed:

interface NotebookInterface { printName(): void;}interface PencilInterface { printName(): void;}interface EraserInterface { printName(): void;}interface StudentInterface { notebook: NotebookInterface; pencil: PencilInterface; eraser: EraserInterface; write(): void; draw(): void;}class Notebook implements NotebookInterface { public printName() {  console.log('this is a notebook'); }}class Pencil implements PencilInterface { public printName() {  console.log('this is a pencil'); }}class Eraser implements EraserInterface { public printName() {  console.log('this is an 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...'); }}

Since the inversity framework is used, we don't need to implement the injector and Inject decorators by ourselves this time. We only need to reference related 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 right? Remember the concepts mentioned in TypeScript in the previous section are just syntactic sugar? Unlike the example in the previous section that passed the constructor reference directly to Inject, because inversify. js is interface-oriented. interfaces such as NotebookInterface and PencilInterface are only syntactic sugar provided by TypeScript and do not exist at runtime, therefore, when declaring dependencies in the decorator, we can only use the string form rather than the reference form. But don't worry. inversify. js provides us with the bind Mechanism, which builds a bridge between the string form of the interface and the specific constructor:

import { TypeBinding, Kernel } from "inversify"; var kernel = new Kernel();kernel.bind(new TypeBinding
 
  ("NotebookInterface", Notebook));kernel.bind(new TypeBinding
  
   ("PencilInterface", Pencil));kernel.bind(new TypeBinding
   
    ("EraserInterface", Eraser));kernel.bind(new TypeBinding
    
     ("StudentInterface", Student));
    
   
  
 

Note that TypeBinding and Kernel need to be introduced from the inversify module in this step. In order to ensure that the return value type and the static type check during compilation can pass smoothly, the generic syntax is also used.
Here, we need to understand the new TypeBinding ("NotebookInterface", Notebook) it is natural that the instance of the Notebook class is provided for Classes dependent on the "NotebookInterface" string, and the return value is traced back to the NotebookInterface.
After completing these steps, it is still easy to use:

var student: StudentInterface = kernel.resolve
 
  ("StudentInterface");console.log(student instanceof Student); // truestudent.notebook.printName(); // this is a notebookstudent.pencil.printName(); // this is a pencilstudent.eraser.printName(); // this is an eraserstudent.draw(); // drawingstudent.write(); // writing
 

The above is all about the four types of dependency injection in the Javascript technology stack. I hope this will help you learn more.

Related Article

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.