With the advent of Node.js, JavaScript has become a common language for both the front and back ends. However, unlike node.js in the front-end domain where there are a number of excellent engineering frameworks such as Angular, React, Vue, etc., in the back-end domain, Express, Koa and other well-known tools failed to solve an important problem — architecture. Nest emerged in this context, inspired by Angular’s design philosophy, and many of Angular’s patterns are derived from the Spring framework in Java, so Nest is the Node.js version of the Spring framework.
Therefore, for many Java backend students, the design and writing methods of Nest are very easy to understand, but for traditional JS programmers from the front end, only mentioning the most important and core ideas of Nest such as inversion of control, dependency injection and other concepts is daunting. Not to mention the concept of TypeScript, decorators, metadata, reflection, etc., and the fact that its official documentation and core community are all in English has left many students out.
This series of articles will start with the design idea of Nest and explain its related concepts and principles in detail, and finally imitate the implementation of an extremely simple (or rudimentary) FakeNest framework. On the one hand, the students who have already used and want to further understand Nest principle can gain something, on the other hand, the students who are engaged in traditional JS front-end development can get started and learn some excellent ideas in back-end development.
This article is the first introduction to Nest. Js, which will detail the most core design ideas in Nest implementation — Inversion of Control (IOC) and dependency injection (DI). We’ll walk you through them step by step with an example of a manufacturing factory-like transformation.
One, manufacturing plant
First, let’s look at what a basic manufacturing factory class would look like.
/ / workers
class Worker {
manualProduceScrew(){
console.log('A screw is built')}}// Screw production workshop
class ScrewWorkshop {
private worker: Worker = new Worker()
produce(){
this.worker.manualProduceScrew()
}
}
/ / factory
class Factory {
start(){
const screwWorkshop = new ScrewWorkshop()
screwWorkshop.produce()
}
}
const factory = new Factory()
// Factory started!!
factory.start()
Copy the code
In this simplified version of the factory, we have designed only three basic classes responsible for the manufacture of screws. At first glance, there was nothing wrong with the design. When the project wanted to produce screws, the factory gave instructions directly to the screw shop, which in turn gave instructions to the workers, and eventually the screws were produced.
However, after a period of time, the factory newly entered a batch of automatic screw production equipment, the factory director hopes to use this batch of equipment to replace workers’ work and reduce production costs, so we need to reform the code of this automobile factory!
/ / machine
class Machine {
autoProduceScrew(){
console.log('A screw is built')}}class ScrewWorkshop {
// Change to a machine instance
private machine: Machine = new Machine()
produce(){
this.machine.autoProduceScrew()
}
}
class Factory {
start(){
const screwWorkshop = new ScrewWorkshop()
screwWorkshop.produce()
}
}
const factory = new Factory()
// Factory started!!
factory.start()
Copy the code
After much effort was made to transform the screw workshop, screws were produced again. But it wasn’t long before the factory manager found and bought cheaper, more efficient machines, and once again we had to revamp the screw shop. Imagine if this situation keeps happening, we need to constantly spend effort to transform the screw production workshop, and soon the director of the production workshop will be impatient with us. Is it possible to replace the underlying production without changing the screw shop?
Ii. Factory renovation
First, let’s analyze what’s wrong with this intuitive design. The Machine/Worker class is the class that finally performs the production action, and they all belong to the screw production workshop ScrewWorkshop. In this respect, the Machine/Worker class should be a low-level class, while ScrewWorkshop should be a high-level class. The high-level classes in the factory depend on the low-level classes. So, we made a big mistake, which was that the design of the plant violated the dependency inversion principle.
Dependency Inversion Principle
High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
High-level modules should not depend on low-level modules; both should depend on abstractions (such as interfaces).
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Abstractions should not depend on details, details (concrete implementations) should depend on abstractions.
Therefore, we should first decouple the Machine/Worker class and ScrewWorkshop class, and let ScrewWorkshop define an interface for the low-level production mode class it wants to use, and let Machine/Worker and other low-level classes follow and implement this interface.
// Define a producer interface
interface Producer {
produceScrew: () = > void
}
// A machine that implements the interface
class Machine implements Producer {
autoProduceScrew(){
console.log('A screw is built')}produceScrew(){
this.autoProduceScrew()
}
}
// The worker that implements the interface
class Worker implements Producer {
manualProduceScrew(){
console.log('A screw is built')}produceScrew(){
this.manualProduceScrew()
}
}
class ScrewWorkshop {
// Rely on the producer interface, can be switched at will!!
// private producer: Producer = new Machine()
private producer: Producer = new Worker()
produce(){
this.producer.produceScrew()
}
}
class Factory {
start(){
const screwWorkshop = new ScrewWorkshop()
screwWorkshop.produce()
}
}
const factory = new Factory()
// Factory started!!
factory.start()
Copy the code
After such a transformation of the factory, the screw production workshop will be significantly easier to transform in the future, just need to change the properties of the new instance that follows the Producer interface. However, this did not completely improve our relationship with the head of the workshop, and we still had to consult the head of the workshop every time we changed the production machine in the factory. ScrewWorkshop still relies on Worker/Machine instances, but less so than before.
So how do you get out of this task by fully complying with the dependency inversion principle? This is where inversion of control and dependency injection come in!
What is Inversion Of Control?
Inversion of control is a design principle. As the name suggests, it is used to reverse different kinds of controls in object-oriented design to achieve loose coupling. In this context, control refers to all of the processes in a class other than the completion of its main workflow, including control over the application flow, as well as the dependency object creation and binding process.
What is Dependency Injection?
Inversion of control tells us what we need to do, but it doesn’t tell us what we should do. So there are many ways to implement inversion of control, among which the most popular means used by Nest, Spring and other mainstream frameworks is dependency injection.
Dependency injection allows you to create dependent objects outside of a class and provide these objects to a class in different ways. Using dependency injection, we can move the creation and binding of the objects that a class depends on out of the implementation of the class itself.
The different methods include constructor injection, property injection, Setter method injection, and interface injection.
I don’t want to look at the concepts, can you just tell me briefly what they do?
Informally, inversion of control and dependency injection provide the following functions:
If class A needs class B, there is no direct control in class A to create an instance of class B. Instead, we control the creation of instances of class B externally from class A, where only instances of class B are used and we don’t care at all about how instances of class B are created.
Below, we will use them for further modifications to our factory.
/ /... The implementation of Worker/Machine and its interface Producer is the same as before, which is omitted here
class ScrewWorkshop {
private producer: Producer
// Inject through the constructor
constructor(producer: Producer){
this.producer = producer
}
produce(){
this.producer.produceScrew()
}
}
class Factory {
start(){
// Control the implementation of producer in the Factory class.
// const producer: Producer = new Worker()
const producer: Producer = new Machine()
// Inject through the constructor
const screwWorkshop = new ScrewWorkshop(producer)
screwWorkshop.produce()
}
}
const factory = new Factory()
// Factory started!!
factory.start()
Copy the code
Finally, we don’t have to bother the workshop director anymore, he can also go off work to play cards and drink freely!
3. “Factory” in Nest
Let’s explain briefly (not exactly, but just to give the reader a sense of emphasis) what we did during the transformation of the workshop:
- ** Dependency inversion: ** removes the dependency relationship between ScrewWorkshop and Worker/Machine classes, and turns to completely rely on Producer interface;
- ** Reversal of control: ** Instantiates the producer that ScrewWorkshop needs to use, and ScrewWorkshop’s control over its dependency Worker/Machine is reversed;
- **ScrewWorkshop does not focus on creating a concrete producer instance, but inject it through the constructor function.
It should be made clear that dependency inversion and inversion of control are both design principles and ideas, while dependency injection is the real means of implementation. In the design of Nest, the idea of inversion of control is followed, and dependency injection (including constructor injection, parameter injection, Setter method injection) is used to decouple the dependency between Controller and Provider.
Finally, we’ll draw an analogy between the elements in Nest and the factory we wrote ourselves:
- Provider & Worker/Machine: Low-level classes that really provide concrete functionality implementations.
- Controller & ScrewWorkshop: A high-level class that calls low-level classes to provide services to users.
- Nest framework itself & Factory: Inversion of control container, unified management of high-level and low-level classes, control the creation and injection of related classes, decoupling the dependency between classes.
Four,
The idea of dependency injection and inversion of control (INVERSION of control) has been a must-learn foundation in the back-end domain, but it has been tepid in the front-end domain. However, learning and understanding of this idea is very helpful for front-end students to cultivate object-oriented programming thinking, so as to write good code with low coupling and high cohesion.
Returning to the main task of this series, learning to understand the ideas of dependency injection and inversion of control is fundamental to understanding the nest.js source code. By combining inversion of control and dependency injection with the use of decorators and metadata, the core design of Nex.js can be easily accomplished. In the next article, we’ll learn what decorators are, what metadata is, and what their combination can do.