Dashi Lives in the Big Front End a collection of front-end technology blog posts can be accessed at:

Github Base [Blog Park] [Huawei Cloud Community] [Nuggets]

Bytedance Happiness big front end team invites all the best to come and play, the team is harmonious and loving, the technology is hardcore, the bytefan is positive, covering the front end of all directions of the technology stack, there is always a place for you, Base Beijing, social recruitment internship has HC, do not hesitate, please directly aim at [email protected]~

[TOC]

Angular is a front-end framework launched by Google. It was once called the “front-end troika” by developers along with React and Vue. However, with the development of technology iterations, Angular’s presence in the domestic front-end technology circle is becoming less and less. It is usually preferred only by engineers on the back end of the Java technology stack who are considering switching to full-stack engineers. Presents the cause of the decline is not because it is not good enough, because it is too good, still have some high cold, ignore the domestic front end developers’ willingness to learn and accept ability, as an outstanding student, clearly result has been very good, but still consistently seek challenges to achieve breakthrough, even though he never stingy to share your thoughts, But he worked in fields that were beyond the reach of many underachievers, who were interested in what he thought was boring, and the end result was usually a game of their own.

Those of you who have read the history of front-end frameworks probably know how popular angular1. x was back in 2014. While it wasn’t the first framework to introduce MVC to the front end, it was certainly the first to really shake up jQuery. The forced use of Typescript as the development language during the Angular2.0 upgrade caused it to lose a lot of users, and Vue and React began to take over, quickly forming a three-way battle. But Angular doesn’t seem to be going back. Instead, it continues to iterate at the rate of a major release every six months to bring new concepts to the front end, driving the technology evolution of the front end and moving the front end closer to formal software engineering. I always say is presents an aloof change, like the concept of the introduction and spread thought it, who is recognized as correct elegant and helpful to the engineering practice to the front, it always seems to be said “this is good, then we can implement it in presents”, from the early modularity and the introduction of two-way data binding, To the later componentization, Typescript, Cli, RxJS, DI, AOT and so on, the introduction of one feature after another leads developers to think from different angles, expands the boundary of the front-end field, and puts forward higher requirements for the overall quality of the team. If you look at where Typescript is in front-end development today, look at the differences between Vue and Angular1.x in the early days, and look at the lag between RxJS and React Hooks, it’s easy to see how forward-thinking Angular was.

“If a thing is a software engineer should know, so you should understand it”, this is in my opinion presents to front-end developers the most valuable ideas, fine division of labor is very beneficial for the enterprise, but it is very easy to limit technical personnel itself horizons and career development, as if the assembly line workers engaged in physical labor, Even if you are familiar with the link you are responsible for, you can’t guarantee the final quality of the whole parts processing only by this. We should be responsible for their output in the collaboration, but only to remove job title from the shackles of thinking, you can make a more comprehensive software engineer, it is not about skills, it’s about ways of thinking, cognitive and positioning will decide that come from deep inside a person can reach the height of the future.

Whether or not you will use Angular in your own projects, hopefully you will take the time to understand its philosophy, which will expand your understanding of programming and the beauty of software technical thinking. In this chapter, we’ll take a look at DI, one of the most distinctive technologies in the Angular framework, IOC design patterns, AOP programming ideas, implementation-level decorator syntax, and finally, how to use Inversify.js to implement dependency injection in your own code. If you’re interested, you can delve deeper into this via Java’s Spring framework.

Why do dependencies need injection

Dependency Injection (DI) is not a complex concept, but it is not easy to understand the principles behind it. There are more abstract IOC design ideas upstream and more concrete AOP programming ideas and decorator syntax downstream. Only by making clear the connection between terms in the whole knowledge context can a relatively complete cognition be established, so as to use it in appropriate scenarios. The relationship of core concepts is shown in the figure below:Object-oriented programming operates on the basis of “classes” and “instances,” and when you want to use the functionality of a class, you usually need to instantiate it before you can call the associated instance methods. Due to follow the principle of “single responsibility” design, developers in the implementation of complex function does not write code all together, but depends on the collaboration more child module to realize the corresponding function, how will these modules together for object-oriented programming is very important, it is also design patterns related knowledge to solve the main problems, The design of code structure not only affects the development efficiency of team collaboration, but also affects the maintenance and extension cost of code in the future. After all, in real development, different modules may be developed and maintained by different teams. If the coupling degree between modules is too high, then once the lower-level modules are changed, the whole program may be modified at the code level very much, which has a very serious impact on the software reliability.

In normal programming mode, developers need to import factory methods of their dependent classes or related classes and manually instantiate and bind submodules, as shown below:

import B from'.. / def/B ';import createC from'.. / def/C ';class A{
    constructor(paramB, paramC){
       this.b = new B(paramB);
       this.c = createC(paramC);
}
actionA(){
  this.b.actionB(); }}Copy the code

There is nothing wrong with this from a functional implementation perspective, but there are some potential risks from a code structure design perspective. First, the construction parameters accepted when generating an instance of A are not actually consumed by A itself, but are passed through and distributed to the classes B and C on which it depends. In other words, A takes on additional instantiation tasks of B and C in addition to its own responsibilities. This is contrary to the “single responsibility” principle of the basic design principle of SOLID in object-oriented programming; Second, instance A of class A depends only on the actionB method of class B instance. If you unit test the actionA method, in theory, the unit test should pass as long as the actionB method executes correctly, but in the example code above, In fact, such a unit test has become a small-scale integration test that includes B instantiation process, C instantiation process and actionB method invocation. Any abnormality in any link will lead to failure of the unit test. Finally, for the C module, it exposed the factory method createC the instantiation of the process can be controlled, such as maintaining a global singletons, but for directly derived class definition B module, every module rely on it to your complete instantiation of it, if the future of class B construction method has changed, I’m afraid developers have to use the IDE to search globally for all code that instantiates class B and then modify it manually.

The pattern of “dependency injection” is in order to solve the above problems and appear, in this kind of programming model, we no longer accept structure parameters and then manually, the instantiation of child module, but directly in the constructor accepts a finished instantiated object, with the basic form of code level becomes a the appearance of the following:

class A{
    constructor(bInstance, cInstance){
       this.b = bInstance;
       this.c = cInstance;
}
actionA(){
  this.b.actionB(); }}Copy the code

For class A, it depends on instances of b and c instance is when the structure from external injection in, that means it will no longer need to care about son module instantiation of the process, and only need A statement in the form of parameter dependence on the instance, and then concentrate on what they are responsible for the function of the can, subsidiary module instantiation of the job to A kind of external other modules to complete, This external module, often referred to as the “IOC container,” is essentially a “class registry + factory method.” Developers register each class with a “key-value” in the IOC container, which then controls the instantiation of the class. When a constructor needs to use an instance of another class, The IOC container automatically does the dependency analysis, generates the required instances and injects them into the constructor, and of course the instances that need to be used in singleton mode are kept in the cache.

On the other hand, in dependency injection mode, the mandatory dependency of the upper class A on the lower modules B and C has been removed, which is very similar to the “duck discrimination” mechanism you know about in JavaScript basics. As long as the actual bInstance parameter passed also implements an actionB method, And on the function signature (or type declaration) and class B actionB method keeps consistent, they are the same for A module, this can greatly reduce the difficulty of unit testing for A module, and convenient developers in the development environment, test environment and production environment and so on different scenarios for the realization of specific modules provide completely different, Rather than being limited by the specific implementation of B modules, if you know the SOLID design principles of object-oriented programming, you will know that “dependency injection” is actually an embodiment of the “dependency inversion principle”.

Dependency Inversion:

  1. Upper-level modules should not depend on lower-level modules, they should depend on common abstractions.

  2. Abstraction should not depend on details, details should depend on abstractions.

This is called the “dependency injection” and “inversion of control” of the basic knowledge, rely on an instance of the change of the pattern generated by the original manual by the IOC container automatic analysis and provided in the form of injection, originally the instantiation process is controlled by the upper module is transferred to the IOC container, they are essentially the realization of the basic design principles of object-oriented method, The goal is to reduce the coupling between modules and hide more details. In many cases, the application of design patterns confuses code that is intuitive and clear, but in return makes the software more resilient to requirements uncertainty. Junior developers should not only focus on implementing immediate requirements, but also think about how to minimize the impact of changing requirements, or even “reverse control” to provide detailed customization to the production staff in the form of configuration files. Keep in mind that the job of a software engineer is to design software and use it and reusable modules to help you fulfill your requirements, not to turn yourself into a tool for moving bricks.

Implementation of IOC containers

Based on the previous analysis, you can think for yourself about what a basic IOC container should do before moving on. The IOC container’s primary responsibility is to take over all instantiation, so it must have access to all class definitions and know the dependencies of each class, but class definitions may be written in multiple different files. How does the IOC container accomplish dependency collection? Method is easier to think of is to implement a registration method the IOC container, developers in each class definition after the completion of the registration method will own constructor and rely on the module name registered to the IOC container, the IOC container maintenance in the form of closure of a private class registry, which in the form of key-value pairs recorded the information of each class, You can add as much configuration information as you need, such as factory methods, dependency lists, whether to use singletons, and pointer attributes to singletons, so that the IOC container has the ability to access all classes and instantiate them. In addition to collecting information, the IOC container also needs to implement a call method to get the required instance. When the call method is executed, it can find the corresponding configuration object based on the incoming key value, and return the correct instance to the caller based on the configuration information. In this way, the IOC container can perform the basic functions of instantiation.

The use of IOC container has a very obvious influence on the coupling relationship between modules. In the original manual instantiation model, the relationship between modules was coupled to each other, and the change of modules could easily lead to the modification of modules that depend on it directly, because the upper modules depend on the lower modules. After the introduction of the IOC container, each class just call vessel registration method of information registration in other modules if there is a rely on to it, by calling the IOC container provides methods to obtain the required instance, as a result, the child module between the instantiation of the process and the main module is no longer a strong dependency, There is no need to modify the main module when the sub-module changes, which is very helpful to ensure the stability of large applications. Now let’s go back to the classic inversion of control diagram and see what’s going on behind it:In fact, the mechanism of IOC is very similar to recruitment. To implement a company’s project, it needs the cooperation of employees from different positions, such as project manager, product, design, RESEARCH and development, and testing. For the company, more attention should be paid to the number of employees needed for each post, and the proportion of employees at different levels, such as low, medium and high, should be about. In this way, the company can evaluate whether the manpower allocation is enough to ensure the project landing from a macro perspective. As for the specific recruited people, the company does not need to care; The role of HR is just like the IOC container. It only needs to search for qualified candidates in the market according to the company’s standards, and check whether they meet the requirements of employment with an interview.

Implement the IOC container manually

We use Typescript to manually implement a simple IOC container class. You can get a feel for its basic usage. Because of its strong typing, it is easier to understand your code at the abstract level. Is a high yield choice for learning. Typescript code adds a lot of restrictions compared to the flexibility of JavaScript, and you may get confused by the type system at first, but as you get used to it, you’ll start to enjoy the engineering clarity that comes with the constraints and discipline at the code level. Let’s start by writing the basic structure and the necessary type constraints:

// IOC member attributes
interface iIOCMember {
  factory: Function;
  singleton: boolean;
  instance?: {}
}

// Define the IOC container
Class IOC {
  private container: Map<PropertyKey, iIOCMember>;

  constructor() {
    this.container = new Map<string, iIOCMember>(); }}Copy the code

In the code above we defined the 2 interfaces and 1 class, the IOC container classes have a private map as an example, its key is PropertyKey type, this is the default type Typescript, refers to the string | number | symbol of joint type, As you can see from the interface definition, it requires a factory method, a property that marks whether a singleton is a singleton, and a pointer to that singleton. Next we add bind to the IOC container class to register the constructor:

// Constructor generics
interface iClass<T> {
  new(... args: any[]): T }// Define the IOC container
class IOC {

  private container: Map<PropertyKey, iIOCMember>;

  constructor() {
    this.container = new Map<string, iIOCMember>();
  }

  bind<T>(key: string, Fn: iClass<T>) {
    const factory = () = > new Fn();
    this.container.set(key, { factory, singleton: true}); }}Copy the code

Bind method of logic is not hard to understand, a beginner could interface for iClass statement is unknown, it refers to the entity implements this interface in is new need to return the default instance of type T, in other words, is receiving a constructor that new () as the property of the interface is also called “literal constructor. But the IOC container is lazily instantiated, and the easiest way to make the constructor execute lazily is to define a simple factory method (as the factory method in the previous example does) and save it for instantiation as needed. Finally, we implement a call method use:

  use(namespace: string) {
    let item = this.container.get(namespace);
    if(item ! = =undefined) {
      if(item.singleton && ! item.instance) { item.instance = item.factory(); }return item.singleton ? item.instance : item.factory();
    } else {
      throw new Error('Constructor not found'); }}Copy the code

The use method receives a string and finds the corresponding value from the container according to it, and the value here will conform to the structure defined by the iIOCMember interface. For the convenience of demonstration, if the corresponding record is not found, the error will be reported directly. If the singleton is needed and no corresponding object has been generated, the factory method will be called to generate the singleton. The configuration information is used to determine whether to return a singleton or create a new instance. Now we can use the IOC container:

class UserService {
  constructor() {}
  test(name: string) {
    console.log(`my name is ${name}`); }}const container = new IOC();
container.bind<UserService>('UserService', UserService);
const userService = container.use('UserService');
userService.test('Da Shi doesn't talk');
Copy the code

When you run the Typescript code directly with TS-Node, you can see the printed information on the console. Above the IOC container just realized the core process, it also does not have the function that rely on management and load, hope you can try to implement, need to do is provide dependent module keys when registration information list, and then when instantiated by recursive way will depend on the modules are mapped to the corresponding instance, You’ll see a similar pattern when you learn about webPack module loading. In the next section, we’ll look at how the Angular1.x version does automatic analysis and injection of dependencies.

Dependency injection in AngularJS

AngularJS refers specifically to older versions of Angular2 (known as Angular in later versions). It advocates the use of modularization to decompose code into modules of Controller, Service, Directive, Filter, etc. In order to improve the structure of the entire code, the Controller module is used to connect the page and the data model, usually there is a Controller for each page, a typical code snippet is as follows:

varApp = presents. The module (" myApp ", []);// Write the page controllerApp. The controller (" mainPageCtrl ",function($scope, userService) {  
     // Controller function operation part, mainly for data initialization and event function definition$scope.title = "$scope. Title"; userService.showUserInfo(); });// Write a custom serviceApp. The service (" userService ",function(){
this.showUserInfo = function()Console.log(' call the method to show user information '); }})Copy the code

The example code defines a global module instance through the module method, and then defines a Controller module and a Service module on the instance. The scope object is used to associate with the page. Variables or event handlers bound by template syntax need to be attached to the scope object of the page to be associated with the page. Variables or event handlers bound by template syntax need to be attached to the page’s scope object to be accessed. When AngularJS runs this simple code, it replaces the ng-bind= “title” tag with custom content. And execute the showUserInfo method on the userService service.

If you look closely at the code above, you can easily see traces of dependency injection. The Controller is defined to receive a string key and a function that receives an external service of the same name with the parameter userService. All the user needs to do is define the module using the AngularJS method. The framework automatically finds the dependent module instance and infuses it when executing the factory method. For the Controller, it simply declares the dependent module in the factory parameter. The app.controller method is essentially the BIND method in the IOC container, which is used to register a factory method into the registry. It simply relies on the collection process, and the app.service method is similar. This implementation is called “inference injection”, which is to infer the dependent module from the name of the parameter passed to the factory method and inject it. The string form of the function body can be obtained by calling the toString method, and then the characters of the parameter, the name of the dependent module, can be extracted using the re. Inference injection is a form of implicit inference that requires the parameter name to be the same as the key name used for module registration, such as userService in the previous example, which corresponds to the userService service defined using the app.service method. This approach is concise, but using the parameter name to find a dependency can cause an error when code is compressed and obtruded using a shorter name. AngularJS provides two other implementations of dependency injection: inline declaration and declaration injection. Their basic syntax is as follows:

// inline injectionApp. The controller (" mainPageCtrl ", [' $scope ', 'userService',function($scope, userService) {  
     // Controller function operation part, mainly for data initialization and event function definition$scope.title = "$scope. Title"; userService.showUserInfo(); }]);// Declare injection
var mainPageCtrl =  function($scope, userService) {  
     // Controller function operation part, mainly for data initialization and event function definition$scope.title = "$scope. Title"; userService.showUserInfo(); }; MainPageCtrl $inject = [' $scope ', 'userService']. App. The controller (" mainPageCtrl ", mainPageCtrl);Copy the code

Inline injection is passing in an array where the factory method was passed. The default array ends with the factory method and precedes with the dependent module’s key. String constants are not subject to compression and obturation like function definitions, so that AngularJS dependency injection can find the required module. The purpose of declare injection is the same, but it will attach the dependency list to the factory function inject property. (JavaScript functions are essentially object types and can add attributes.) The app.controller method receives an array or a function as the second argument, and if it is a function, whether it has the inject attribute. The app.controller method receives the second parameter as an array or function, and if it is a function, whether it has the inject attribute. (Functions in JavaScript are essentially object types. The app.controller method receives the second parameter is an array or a function, and if it is a function, whether it has the inject attribute. Then extract the dependent array and iterate through the loading module.

The AngularJS dependency injection module source code can be found in the SRC/Auto/Injector.js folder named for automated dependency injection. The folder contains a large number of comments from the official documentation, which will help you understand the source code. You can find the annotate method definition there, and you can see the compatible handling of AngularJS for each of the different dependency declarations, but you can do the rest for yourself.

AOP and decorators

Aspect Oriented Programming (AOP) is a very classical idea in Programming. It adds new functions to modules that have been written by means of precompilation or dynamic proxy, thus avoiding the modification of source code. It also makes it easier for developers to separate business logic functions from system-level functions such as logging, transaction processing, performance statistics, behavior burying points, and so on, improving code reuse and maintainability. Projects in real development can take a long time and involve changing people, and writing all of this code together can be disruptive to other collaborators’ understanding of the main business logic. Object-oriented programming focuses on sorting out entity relationships. It solves the problem of how to divide specific requirements into relatively independent and well-packaged classes, and let them have their own behaviors. The focus of section-oriented programming is to strip out common functionality and have many classes share a behavior that only needs to be modified when it changes, allowing developers to focus more on the task of implementing a class’s features rather than worrying about which class it belongs to.

Aspect programming is not a disruptive technology, but a new way of organizing code. Suppose you use the famous AXIos library to handle network requests in your system. The backend returns a token after the user logs in successfully, and you need to add it to the request header for authentication each time you send a request. The general idea is to write a generic getToken method. Then pass it in with custom headers on each request (assuming the custom header field is X-token) :

import { getToken } from'. / utils'; Axios. Get ('/API/report/get_list, {headers: {' X-ray Token: getToken ()}});Copy the code

From the perspective of functional implementation, the above approach is feasible, but we have to reference the public method getToken in every module that needs to send requests, which is very tedious. After all, the action of adding token information in different requests is the same. In contrast, the Axios interceptors mechanism is perfect for handling a similar scenario, which is a very typical “section-oriented” practice:

axios.interceptors.request.use(function (config) {
    // Add custom information to the config configurationconfig.headers = { ... Config. Headers, 'X-ray Token: getToken ()}return config;
  }, function (error) {
    // A handler for an error in the request
    return Promise.reject(error);
  });
Copy the code

If you understand, express and koa framework used in the middleware model, it is easy to realize the interceptor mechanism here and they are essentially the same, user-defined handlers are added in turn into the interceptor array, before the request or response back after specific “time slice” executed in sequence, as a result, Each specific request no longer needs to deal with the non-business logic of adding tokens to the request header, the functional level of the code is stripped and hidden, and the business logic of the code naturally becomes more concise.

In addition to the use of programming skills, high level language also provides more concise syntax for convenient developers practice “aspect oriented programming”, standard support JavaScript from ES7 decorator syntax, but with the current Babel compile tools were found to exist in the front-end engineering, so for the developers do not need to consider the browser problem of new syntax support, With Typescript, developers can implement the logic by configuring parameters in tsconfig.json to enable the decorator (called annotation in the Spring framework) syntax, which is essentially a syntactic sugar. Common decorators include class decorators, method decorators, attribute decorators, and parameter decorators. Almost all parts of a class definition can be wrapped by decorators. The class decorator accepts arguments from classes that need to be modified. This example uses the @testable decorator to add a testable property to a prototype object that is already defined. The testable testable testable testable testable testable object is stable from the like.

function testable(target){
    target.prototype._testable = false;
}
// Write a decorator on the line above the class name
@testable
Class Person{
    constructor(name){
      this.name = name; }}Copy the code

From the code above you will find that even in the absence of a decorator syntax, ourselves in JavaScript execution testable function can also be completed on the propagation of the class, the difference is performed manually packaging statement is imperative style, and decorator syntax is declarative style, which are generally considered more suitable for use in object-oriented programming, This is because it keeps the business logic layer code simple and hands off unimportant details to specialized modules. Decorators provided in Angular usually accept arguments. We simply implement a decorator factory with higher-order functions that return a decorator generator:

// Component definition in Angular
@Component({
    selector: 'hero - the detail,templateUrl: 'hero - detail. HTML',styleUrls: [' style.css ']}) Class MyComponent{/ /...
}

The @component decorator is defined roughly as follows
function Component(params){
    return function(target){
       // Target can access the content in paramstarget.prototype._meta = params; }}Copy the code

This allows the component, when instantiated, to obtain the configuration information passed in from the decorator factory, which is often referred to as the meta information of the class. Other types of decorators work in the same way, but with different arguments in the function signature. For example, a method decorator is called with three arguments:

  • The first argument is a constructor when it decorates a static method, and a prototype object when it decorates a class method

  • The second parameter is the member name

  • The third parameter is the member property descriptor

You might immediately notice that this is the same function signature as Object. DefineProperty in JavaScript, which means that the method decorator is one of the more abstract but versatile methods. In the function body of methods a decorator, we can get from the constructor or the prototype object needs to be adornment method, then use the proxy pattern to generate a new method with additional features, and at the right time to implement the original method, finally through direct assignment or getter in the use of property descriptors to return to new methods of packaging, This extends the functionality of the original method, which you can learn about in the data hijacking section of the Vue2 source code. A method decorator that prints debugging information on the console before and after the decorated method is executed. The code is roughly as follows:

function log(target, key, descriptor){
    const originMethod = target[key];
    const decoratedMethod = () = >{
        console.log(' method before execution ');const result = originMethod();
        console.log(' method executed ');return result;
}
// return the new method
target[key] = decoratedMethod;
}
Copy the code

You can simply mark the decorated method with @log on the line, or you can pass in the log contents as parameters through the factory method. The other types of decorators are not covered in this article; they work similarly, and in the next section we’ll look at how Inversify.js uses the decorator syntax to implement dependency injection.

Use inversify.js to implement dependency injection

Inversify.js provides a more complete dependency injection implementation, written in Typescript.

The basic use

The official website already provides basic sample code and usage, starting with the interface definition:

// file interfaces.ts

export interface Warrior {
    fight(): string;
    sneak(): string;
}

export interface Weapon {
    hit(): string;
}

export interface ThrowableWeapon {
    throw(): string;
}
Copy the code

The fighter, weapon, and throwable interfaces were defined and exported in the code above, remember? Dependency injection is a practice of the dependency inversion principle of the “SOLID” design principle. Upper-layer and lower-layer modules should rely on a common abstraction. When different classes implement interfaces using the implements keyword or declare the type of an identifier as an interface, the structural restrictions of the interface declaration need to be met. Interfaces become their “common abstraction,” and Typescript interface definitions are only used for type constraints and validation and disappear before being compiled into JavaScript. Next is the type definition:

// file types.ts
const TYPES = {
    Warrior: Symbol.for("Warrior"),
    Weapon: Symbol.for("Weapon"),
    ThrowableWeapon: Symbol.for("ThrowableWeapon")};export { TYPES };
Copy the code

Unlike the interface declaration, the type definition here is an object literal that does not disappear when compiled, and inversify.js needs to use it as a module identifier at runtime, as well as string literals, as we did with our own IOC container implementation in the previous article. Next comes the declaration of the class definition:

import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Weapon, ThrowableWeapon, Warrior } from "./interfaces";
import { TYPES } from "./types";

@injectable()
class Katana implements Weapon {
    public hit() {
        return "cut!";
    }
}

@injectable()
class Shuriken implements ThrowableWeapon {
    public throw() {
        return "hit!";
    }
}

@injectable()
class Ninja implements Warrior {

    private _katana: Weapon;
    private _shuriken: ThrowableWeapon;

    public constructor(@inject(TYPES.Weapon) katana: Weapon, @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }}export { Ninja, Katana, Shuriken };
Copy the code

Injectable and Inject are the same terms used in most dependency injection frameworks. Injectable means injectable, which tells the dependency injection framework that the class needs to be registered with the container. Inject into the meaning, it is a decoration factory, accept the parameters is already defined in the types of type name, if you feel difficult to understand here, it can be treated as a string directly, its role is also to inform framework for the dependent variable at according to which the key is to find the corresponding module, If you compare this syntax to dependency injection in AngularJS, it no longer requires the developer to manually maintain dependency arrays. The last thing we need to deal with is the container configuration:

// file inversify.config.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";

const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);

export { myContainer };
Copy the code

Don’t get bogged down by Typescript’s complexity, we use the same IOC container class we implemented earlier, except that the API we use is ioc.bind(key).to(value), Finally, we can use the IOC container example:

import { myContainer } from "./inversify.config";
import { TYPES } from "./types";
import { Warrior } from "./interfaces";

const ninja = myContainer.get<Warrior>(TYPES.Warrior);
expect(ninja.fight()).eql("cut!"); // true
expect(ninja.sneak()).eql("hit!"); // true
Copy the code

Inversify.js provides a get method to get a specified class from the Container, so you can use the Container instance to manage classes in your project in code that can be found in the code repository in this chapter.

Source analyses

In this section, we delve into the source code level to explore some of the problems that many readers may find daunting at the mention of source code, but the implementation of the inversify.js code level may be much simpler than you think, but it still takes a lot of time and effort to figure out the thinking behind the framework structure. The injectable decorator is defined first:

import * as ERRORS_MSGS from ".. /constants/error_msgs";
import * as METADATA_KEY from ".. /constants/metadata_keys";
function injectable() {
    return function (target) {
        if (Reflect.hasOwnMetadata(METADATA_KEY.PARAM_TYPES, target)) {
            throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
        }
        var types = Reflect.getMetadata(METADATA_KEY.DESIGN_PARAM_TYPES, target) || [];
        Reflect.defineMetadata(METADATA_KEY.PARAM_TYPES, types, target);
        return target;
    };
}
export { injectable };
Copy the code

The Reflect Object is a global Object defined in the ES6 standard to provide a functionalized implementation of the API originally mounted on the Object.prototype Object. The reflect.definemetadata method is not a standard API. Rather, it is an extended capability provided by the introduced Reflect-Metadata library, also known as “meta-information,” which is usually additional information unrelated to business logic that needs to be hidden inside a program. If implemented by ourselves, there is a high probability that an attribute named _metadata will be directly mounted on the object, but with reflect-metadata, the key-value pairs of the metadata will be mapped to the entity object or object attributes, thus avoiding contamination of the target object. It is used as follows:

// Add meta information for the class
Reflect.defineMetadata(metadataKey, metadataValue, target);
// Add meta information for class attributes
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
Copy the code

The METADATA_KEY object introduced in the Injectable source code is really just a string. This is easy to understand when you replace the constant identifiers in the above code with the corresponding strings:

function injectable() {
    return function (target) {
        if (ReflectHasOwnMetadata (' inversify: paramtypes', target)) {throw new Error(/ *... * /);
        }
        var types = ReflectFor getMetadata (' design: paramtypes', target) | | [];ReflectDefineMetadata (' inversify: paramtypes' types, the target).return target;
    };
}
Copy the code

As you can see, what the Injectable decorator does is assign the meta information with the target key ‘Design: Paramtypes’ to the meta information with the key’ Inversify: Paramtypes’. Inject Decorator factory inject decorator factory

function inject(serviceIdentifier) {
    return function (target, targetKey, index) {
        if (serviceIdentifier === undefined) {
            throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name));
        }
        var metadata = new Metadata(METADATA_KEY.INJECT_TAG, serviceIdentifier);
        if (typeof index === "number") {
            tagParameter(target, targetKey, index, metadata);
        }
        else{ tagProperty(target, targetKey, metadata); }}; }export { inject };
Copy the code

Inject is a decorator factory, the logic is based on the incoming identifier (that is, in this paper, we define the types of), instantiate a meta information object, and then, according to the type of the parameter to invoke the different processing function, when the decorator as parameters a decorator, the third parameter index is the order of the this parameter in the function parameter index, TagParameter and tagProperty are underlying calls to the same function. The core logic is to add the new meta information to the correct array after a lot of fault-tolerant checking. As a matter of fact, injectable and Inject serve as decorators to preserve meta information. IOC’s instance management capability relies on the Container class.

Container class in Inversify.js decomposes the instantiation process into multiple customized stages, and adds many extension mechanisms such as multi-container management, multi-value injection, and custom middleware. The source code itself is not difficult to read, but it is relatively theoretical and has many English terms. The Container class is of limited utility to beginners and mid-level developers, so I’m not going to go into the details of the Container class implementation in this article. There are plenty of articles in the community that analyze the source code structure in detail to help those who are interested.

stop

If you first contact the dependency injection related knowledge, could also be and at the beginning, the author think that the theory and method of a very “senior”, can’t wait to want to understand, in fact even spend a lot of time to browse the source code, I also in the actual work hardly ever used it, but left the consciousness of the “decoupling” in my mind. As software engineers, we need to understand the principles and ideas behind technology in order to expand our thinking, but reverence for technology should not degenerate into idolaty for advanced technology. “Dependency injection” is just a kind of design patterns, there will always be it suitable or not suitable for the mode of usage scenarios, there are many common design model, classic design also has a lot of, can only make flexible use of himself in the code structure for the work of the organization, please don’t let obsession limit the breadth of his thinking.