1 introduction

Nest provides a module mechanism that enables dependency injection by defining providers, imports, exports, and provider constructors in a module decorator, organizing the development of the entire application through a module tree. There is no problem in simply lifting an application according to the framework’s conventions. However, it seems to me that there is a lack of a clearer systematic understanding of the dependency injection, inversion of control, modules, providers, metadata, associated decorators, and so on that the framework claims.

  • Why inversion of control?
  • What is dependency injection?
  • What does the decorator do?
  • What are the implementation principles of @Module providers, imports, exports?

Seems to be able to understand, can understand, but let me make myself clear from the beginning, I can’t say. This article was the result of some exploration. From now on, let’s start over and get into the text.

2 Two Stages

2.1 Express, Koa

The development of a language and its technical community must be enriched from the bottom up, just as the roots grow into branches and then grow into leaves. Early on, Nodejs introduced basic Web services frameworks like Express and Koa. Able to provide a very basic service capability. Based on this framework, a large number of middleware and plug-ins began to emerge in the community to provide richer services for the framework. We need to organize application dependencies and build application scaffolding ourselves, which is flexible and tedious and has a certain amount of work.

Later, more efficient and consistent frameworks were created, ushering in a period of renewal.

2.2 EggJs, Nestjs

EggJs, NestJs, Midway and other frameworks were developed out of the box in order to better adapt to rapid production applications and unify specifications. This kind of framework abstracts the implementation of an application into a general and extensible process by implementing the underlying life cycle. We just need to follow the configuration provided by the framework, so that we can implement the application more easily. The framework realizes the process control of the program, and we only need to assemble our parts in the right place. It looks more like assembly line work, each process is clearly separated, and saves a lot of implementation cost.

2.3 summary

The above two stages are just a preparation, we can roughly understand that the framework upgrade improves productivity, and to realize the framework upgrade, some design ideas and patterns will be introduced, Nest has inversion of control, dependency injection, metaprogramming concepts, let’s talk about.

Inversion of control and dependency injection

3.1 Dependency Injection

An application is actually a lot of abstract classes that call each other to perform all of the application’s functions. As application code and functionality increase in complexity, projects are bound to become more difficult to maintain because of the increasing number of classes and their relationships with each other.

For example, if we use Koa to develop our application, Koa itself mainly implements a basic set of Web services capabilities. As we implement the application, we will define many classes, and the instantiation of these classes, and their interdependencies, will be organized and controlled by our code logic. Each class is instantiated manually by us, and we can control whether a class is instantiated once and then shared, or instantiated every time. The following class B depends on A, which is instantiated every time B is instantiated, so for each instance B, A is the unshared instance.

class A{} // B class B{ contructor(){ this.a = new A(); }}Copy the code

The C below is the external instance fetched, so multiple C instances are shared with the app.a instance.

class A{} // C const app = {}; app.a = new A(); class C{ contructor(){ this.a = app.a; }}Copy the code

The following D is passed in as a constructor argument, either one non-shared instance at a time, or a shared instance of app.a (D and F share app.a), and since it is now passed in as a parameter, I can also pass in an instance of class X.

class A{}
class X{}
// D
const app = {};
app.a = new A();
class D{
    contructor(a){
        this.a = a;
    }
}
class F{
    contructor(a){
        this.a = a;
    }
}
new D(app.a)
new F(app.a)
new D(new X())
Copy the code

This method is called dependency injection, in which A, on which B depends, is injected into B by passing values. Constructor injection is just one way to do it. You can also pass in a set call, or any other way to pass in an external dependency. It’s really that simple.

class A{}
// D
class D{
    setDep(a){
        this.a = a;
    }
}
const d = new D()
d.setDep(new A())
Copy the code

3.2 All in Dependency Injection?

As the iteration goes on, the dependence of B will change according to different preconditions. For example, precondition 1 this.a needs to pass in an instance of A, and precondition 2 this.a needs to pass in an instance of X. At this point, we’ll start to do the actual abstraction. We will change to dependency injection like D above.

At the beginning, when we implemented the application, we realized the writing method of CLASS B and C on the condition that we met the requirements at that time. There was no problem in this itself. After several years of project iteration, this part of the code might not be touched. If we think about late expansion and so on, it will affect the development efficiency, and not necessarily useful. So most of the time, we will encounter a scenario that needs to be abstracted, and then we will abstract part of the code.

Class B{contructor(){this.a = new a (); }} new B() {class D{contructor(a){this.a = a; } } new D(new A()) new D(new X())Copy the code

According to the current development mode, all three types of CBD exist, and B and C have a certain probability to develop into D. Every time we upgrade the abstract process of D, we need to reconstruct the code, which is an implementation cost.

The purpose of this example is to illustrate that in a development mode without any constraints or rules. We are free to write code to achieve dependencies between classes. In a completely open environment, it is very free, this is a primitive age of slash-and-burn. Without a fixed code development model, not a maximum programme of action, as different developers involved in the difference of different time periods or the same developers write code, the code in the process of growth, the dependencies will become very not clear, the Shared instance may be instantiated for many times, the waste of memory. A complete dependency structure can be difficult to discern from the code, which can become very difficult to maintain.

If we define each class, we write it in the dependency injection way, and write it as D, then the abstraction process of C and B will be advanced, so that the later extension will be more convenient, and the transformation cost will be reduced. So we call this All in dependency injection, which means that All of our dependencies are implemented through dependency injection.

However, the cost of implementation becomes high in the early stage, and it is difficult to achieve unity and persist in team collaboration, which may eventually fail. This can also be defined as excessive design, because the extra implementation cost may not bring benefits.

3.3 Inversion of control

Since the unified use of dependency injection has been agreed, can a low-level controller be implemented through the low-level encapsulation of the framework and a dependency configuration rule be agreed? The controller can control the instantiation process and dependency sharing according to the dependency configuration defined by us, so as to help us achieve class management? This design pattern is called inversion of control.

Inversion of control may be hard to understand when you first hear about it, what does control mean? Reversed what?

The guess is that developers started with such frameworks and didn’t experience the “Express, Koa era” of old-world beatings. With this reversal of the word, in the program appears very abstract, difficult to read.

Previously we said that the implementation of Koa application, all classes are completely controlled by our free, so it can be regarded as a routine program control way, that is called: control forward. We use Nest, which implements a set of controllers at the bottom. We just write configuration code during actual development, and the framework program manages the dependency injection for our classes, so we call it inversion of control.

The essence is to give the implementation process of the program to the framework program to unified management, control from the developer, to the framework program.

Control forward: the developer controls the program purely manually

Inversion of control: frame program control

Take a realistic example of a person who drives to work with the intention of getting there. It drives itself and controls its own route. If he gives up the control of driving, he will catch the bus, and he only needs to choose a corresponding bus to arrive at the company. In terms of control alone, people are liberated, just need to remember to take the bus on the line, the probability of making mistakes is smaller, people are also much more relaxed. The bus system is the controller and the bus line is the convention configuration.

From the actual comparison above, I think I can understand inversion of control a little bit.

3.4 summary

From Koa to Nest, from JQuery to Vue React. In fact, step by step through the framework encapsulation, to solve the problem of low efficiency in the last era.

The above Koa application uses a very primitive way to control dependencies and instantiations, just like JQuery in the front-end to manipulate the DOM. This very primitive way is called control forward, while Vue React provides a layer of program controllers, just like Nest, which can be called inversion of control. This is also a personal understanding, if there is a problem expect the great god to point out.

Let’s move on to the @Module in Nest, which is needed as a medium for dependency injection and inversion of control.

4 Nestjs Module (@Module)

Nestjs implements inversion of control, with the convention to configure @ Module’s imports, exports, and providers management providers (i.e., dependency injection of classes).

Providers can be understood as registering and instantiating classes in the current module. The following A and B are instantiated in the current module. If B references A in A constructor, it is the referenced A instance of the current ModuleD.

import { Module } from '@nestjs/common'; import { ModuleX } from './moduleX'; import { A } from './A'; import { B } from './B'; @Module({ imports: [ModuleX], providers: [A,B], exports: [A] }) export class ModuleD {} // B class B{ constructor(a:A){ this.a = a; }}Copy the code

Exports are classes that instantiate providers in the current module as classes that can be shared by external modules. For example, when class C of ModuleF is instantiated, we want to inject class A instances of ModuleD directly. Set export (Exports) A in ModuleD and import ModuleD via imports in ModuleF.

In the following way, the inversion of control program automatically scans for dependencies. First, it looks for the providers of its module to see if there is an instance of A in the imported ModuleD. If not, it looks for an instance of A in the imported ModuleD to inject into the C instance.

import { Module } from '@nestjs/common'; import { ModuleD} from './moduleD'; import { C } from './C'; @Module({ imports: [ModuleD], providers: [C], }) export class ModuleF {} // C class C { constructor(a:A){ this.a = a; }}Copy the code

To make an external module use an instance of the module’s class, you must first define the instantiation class in the module’s providers and then export the class. Otherwise, an error will be reported.

//正确
@Module({
  providers: [A],
  exports: [A]
})
//错误
@Module({
  providers: [],
  exports: [A]
})
Copy the code

Here is a mouth of TS knowledge points

export class C {
  constructor(private a: A) {
  }
}
Copy the code

Since TypeScript supports constructor arguments (private, protected, public, readonly) to be implicitly and automatically defined as class properties, Therefore, this. A = a is not required. Nest is written like this.

5 Nest metaprogramming

The concept of metaprogramming is embodied in Nest framework, in which the inversion of control, decorator, is the realization of metaprogramming. Metaprogramming is essentially programming, but with an abstract program in the middle that recognizes metadata (such as object data in @Module) as an extension of the ability to process other programs as data. When we write abstract programs like this, we’re metaprogramming.

5.1 the metadata

Metadata is also often mentioned in Nest documentation. The concept of metadata can be confusing when you first see it. You need to get used to it over time, so you don’t have to worry too much.

Metadata is defined as: data describing data, mainly information describing data attributes, can also be understood as data describing programs.

The exports, providers, imports, and controllers of the @Module configuration in Nest are all metadata, because it describes application relationships. This data is not presented to the end user, but is read and identified by the framework application.

5.2 Nest Decorator

If you look at the Nest decorator source code, you’ll see that almost every decorator itself just defines a metadata through reflect-Metadata.

@ Injectable decorator

export function Injectable(options? : InjectableOptions): ClassDecorator { return (target: object) => { Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target); }; }Copy the code

The @Module decorator is an example of how to define a metadata provider. Instead, we pass a class into the providers array. When the application is actually running, the providers class will be automatically instantiated into a provider. There is no need for developers to display instantiation and dependency injection. A class becomes a provider only after it is instantiated in a module. Providers’ classes are reflected as providers, and inversion of control uses reflection techniques.

Another example is the ORM (object relational mapping) in the database. Using ORM, you only need to define the table fields, and the ORM library automatically converts the object data into SQL statements.

const data = TableModel.build();

data.time = 1;
data.browser = 'chrome';
    
data.save();
// SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":"chrome"}]
Copy the code

ORM library is the use of reflection technology, so that users only need to pay attention to the field data itself, the object is reflected by ORM library into SQL execution statements, developers only need to pay attention to the data field, and do not need to write SQL.

5.3 reflect – metadata

Reflect-metadata is a reflection library that Nest uses to manage metadata. Reflect-metadata uses WeakMap to create a global singleton and set and get the metadata of the decorated object (class, method, etc.) using set and GET methods.

Var _WeakMap =! usePolyfill && typeof WeakMap === "function" ? WeakMap : CreateWeakMapPolyfill(); var Metadata = new _WeakMap(); function defineMetadata(){ OrdinaryDefineOwnMetadata(){ GetOrCreateMetadataMap(){ var targetMetadata = Metadata.get(O); if (IsUndefined(targetMetadata)) { if (! Create) return undefined; targetMetadata = new _Map(); Metadata.set(O, targetMetadata); } var metadataMap = targetMetadata.get(P); if (IsUndefined(metadataMap)) { if (! Create) return undefined; metadataMap = new _Map(); targetMetadata.set(P, metadataMap); } return metadataMap; }}}Copy the code

Reflect-metadata stores the decorator’s metadata in a global singleton for unified management. Reflect-metadata does not implement specific reflection, but rather provides a library of tools to assist reflection implementation.

6 the end

Now let’s take a look at the first few questions.

  1. Why inversion of control?
  2. What is dependency injection?
  3. What does the decorator do?
  4. What are the implementation principles of @Module providers, imports, exports?

I think 1 and 2 have been made clear before. If there is still some ambiguity, I suggest you go back and look up some other articles to help you understand the knowledge through the thinking of different authors.

6.1 Problems [3 4] Summary:

Nest uses reflection technology to implement inversion of control and provides metaprogramming capabilities, where developers use the @Module decorator to decorate classes and define metadata (providers\imports\exports) that is stored in global objects (using the Reflect-Metadata library). After the program runs, the controller inside the Nest framework reads and registers the module tree, scans the metadata and instantiates the class as a provider, and looks for instances of other dependent classes (providers) of the current class among the providers of all modules, according to the providers\imports\ Exports definition in the module metadata. It is found and injected through the constructor.

There are many concepts in this paper, and no detailed analysis is made. It takes time to understand the concepts. If you don’t understand them thoroughly for a while, there is no need to worry too much. Ok, here, this article or spend a lot of energy, like friends expect you to be able to connect three keys ~