Concept: Dependency injection (DI), Inversion of Control (IOC), IOC container

Dependency injection (DI) and inversion of control (IOC) are basically the same thing because you can’t do without each other. In simple terms, class A depends on class B, but A does not control the creation and destruction of B. Only B is used, so control of B is handed over to A. This is called inversion of control (IOC). Since A depends on B, instance B must be used in A. We can inject an instance of B via A’s constructor, for example:

class B {}class A {
  constructor(b: B) { 
      console.log(b); }}const b = new B();
// Inject an instance of B into a
const a = new A(b);
Copy the code

This process is called dependency injection (DI). So what is an IOC Container? In the example above, injecting the instance of B into the constructor of A is A manual and cumbersome process, especially when the class relationship becomes variable and complex, which is difficult to maintain. Therefore, IOC container is to solve such problems. IOC container is responsible for managing the life cycle and dependency relationship of objects, and realizing the dependency search and dependency injection of objects. For example, Java’s Spring and the dependency injector (DI) for the front-end @Angular framework belong to the IOC container.

Next I’ll look at some of the benefits of using dependency injection versus non-dependency injection in code.

Non-dependency injection code

Let’s start with a piece of traditional implementation code (non-DI) car.ts

/ / the engine
export class Engine {
  public cylinders = 'Engine Engine 1';
}
/ / tire
export class Tires {
  public make = 'brand';
}
export class Car {
  public engine: Engine;
  public tires: Tires;
  public description = 'No DI'; 
  constructor() {
    this.engine = new Engine();
    this.tires = new Tires();
  }
  // Method using the engine and tires
  drive() {
    return `The ${this.description} car with ` +
      `The ${this.engine.cylinders} cylinders and The ${this.tires.make} tires.`; }}Copy the code

The Car creates an engine and tires without any third-party container. This is highly coupled and causes the following problems:

Problem 1: If the engine is to be upgraded one day, the code is as follows:

/ / the engine
export class Engine {
  public cylinders = ' ';
  constructor(_cylinders:string) {
    this.cylinders = _cylinders; }}Copy the code

The new Engine(parameter) in the Car class has been modified so that the new Engine(parameter) does not need to be modified. (Answer: DI)

Question 2: If you want to use a different brand of tyre on the Car, the code is as follows:

/ / tire
export class Tires {
  public make = 'brand';
}
export class Tires1 extends Tires {
  public make = Brands' 1 ';
}
export class Tires2 extends Tires {
  public make = Brands' 2 ';
}
export class Car {
   //... Other code omitted...
  public tires: Tires;
  constructor() {
    this.tires = newTires1(); }}Copy the code

The Car code needs to be changed again. Here’s a question: How can we change the Car without changing the Car class? (Answer: DI)

Question 3: How to realize data sharing? For example, the Internet of vehicles has established a Service data center, and different cars realize data communication and data sharing through Service. Data sharing and communication cannot be realized through new Service in Car. Because services in different cars are not the same instance.

Consider a question: How do you implement data communication and sharing between different cars?

Problem 4: Testing is difficult and impossible. In the sample code, The Car class is dependent on Engine and Tires. Engine and Tires may be dependent on other classes, and any other classes may have more dependencies. Without controlling the hidden dependencies behind the Car, it is difficult to test. Such code is simply untestable. For example, if you want to test the performance of a car with different brands of wheels at the same time, you can’t do it because new is already written in the car. For example, if you want to test the performance of a car with different engine parameters at the same time, you cannot do this because new is already written in the car. Unless you’re testing one brand at a time, here’s an example of testing different brands of wheels: Start with brand 1 wheels: Car.ts

export class Tires {
  public make = 'brand';
}
export class Tires1 extends Tires {
  public make = Brands' 1 ';
}
export class Tires2 extends Tires {
  public make = Brands' 2 ';
}
export class Car {
  public tires: Tires;
  public description = 'No DI'; 
  constructor() {
    // New a brand 1 wheel
    this.tires = new Tires1();
  }
  // Method using the engine and tires
  drive() {
    return `The ${this.description} car with ` + ` The ${this.tires.make} tires.`; }}Copy the code

Test program car.spec.ts

import { Car } from './car.ts';

describe('Car unit Tests'.function () {
  it('Test the drivability of the Car on brand 1 wheels'.function () {
    const car = new Car();
    car.drive().should.equal('No DI car with brand 1 Tires.'); })})Copy the code

The above code is used to test the driving performance of the car of brand wheel 1. Tires = New Tires1(); Tires = New Tires1; This. Tires = new Tires2(); Output the drivability of the Wheel brand 2 car.

Such test efficiency is very low, because every time can only manual test case, if coupled with the engine test, the variety of mixed situation even more, won’t do automatic test, the so-called automatic test, is a one-time write all the situation to a unit test, run at a time, all conditions will be test, when the test passed, Then the code does what it’s supposed to do.

To address these issues, let’s look at the benefits of using DI.

Using Dependency Injection (DI)

Next, I’ll demonstrate using DI to solve the above four problems. Let’s start with the car. Ts code that uses DI: car

export class Engine {
  public cylinders = 'Engine Engine 1';
}
export class Tires {
  public make = 'brand';
}
export class Tires1 extends Tires {
  public make = Brands' 1 ';
}
export class Tires2 extends Tires {
  public make = Brands' 2 ';
}
export class Car {
  public description = 'DI'; 
  // Inject Engine and Tires through the constructor
  constructor(public engine: Engine, public tires: Tires) {}  
  // Method using the engine and tires
  drive() {
    return `The ${this.description} car with ` +
      `The ${this.engine.cylinders} cylinders and The ${this.tires.make} tires.`; }}Copy the code

In the above code, we create Car by passing engine and Tires into the constructor. The Car class no longer creates engine and Tires. Instead, it consumes them. For new cars, any Engine and Tires(let Car = new Car(new Engine(),new Tires()));

Fix problem # 1: If you upgrade the engine one day, the code looks like this:

export class Engine {
  public cylinders = ' ';
  constructor(_cylinders:string) {
    this.cylinders = _cylinders; }}Copy the code

We need to pass in a parameter when creating the engine. We don’t need to change the Car class, just the main program:

Main program code:

main(){
    const car = new Car(new Engine('Engine starter 2'), new Tires1());
    car.drive();
}
Copy the code

Solution 2: If you want to use a different brand of tyre on the Car, the code is as follows:

export class Tires {
  public make = 'brand';
}
export class Tire1 extends Tires {
  public make = Brands' 1 ';
}
export class Tire2 extends Tires {
  public make = Brands' 2 ';
}
export class Car {
   //... Other code omitted...
  constructor(public engine: Engine, public tires: Tires) {}  
}
Copy the code

There is no need to modify the Car class, just modify the main program:

main(){
  // Use brand 2 tires
  const car = new Car(new Engine('Engine starter 2'), new Tires2());
  car.drive();
}
Copy the code

Solution 3: How to implement data sharing, such as the Internet of cars, with a Service data center (like Angular’s Service layer, which can be shared with multiple components), where different cars communicate and share data through services. The code is as follows: service.ts

export class Service {
  public data = ' ';
  // Save data to Service
  setData(_data: string) {
    this.data = _data;
  }
  // Fetch data from Service
  getData() {
    return this.data; }}Copy the code

car.ts

export class Car {
  constructor(public service: Service) { }
  // Save data to Service
  setDataToService(_data: string) {
    this.service.setData(_data);
  }
  // Fetch data from Service
  getDataFromService() {
    return this.service.getData(); }}Copy the code

The main program is as follows:

main(){
  // Create a shared Service center
  const shareService = new Service();
  const car1 = new Car(shareService);
  const car2 = new Car(shareService);
  // Car1 stores data to the service center
  car1.setDataToService('this data is from car1.');
  // Car2 retrieves data from the service center
  car2.getDataFromService();
}
Copy the code

In the sample code, The Car class is dependent on Engine and Tires. Engine and Tires may be dependent on any other class, and any other classes may have their own dependencies. Testing DI is easy. The test program is as follows: Test program car.spec.ts

import { Car,Engine,Tires1, Tires2} from './car.ts';
// Test the program entry
describe('Car unit Tests'.function () {
  const engine1 = new Engine('Engine Engine 1');
  const engine2 = new Engine('Engine engine 2');
  const tires1 = new Tires1();
  const tires2 = new Tires2();

  it('Test engine 1 Tyre Brand 1'.function () {
    const car = new Car(engine1, tires1);
    car.drive().should.equal('DI car with engine engine 1 cylinders and brand 1 tires.');
  });
  it('Test engine 1 Tyre brand 2'.function () {
    const car = new Car(engine1, tires2);
    car.drive().should.equal('DI car with engine engine 1 cylinders and brand 2 tires.');
  });
  it('Test engine 2 Tyre Brand 1'.function () {
    const car = new Car(engine2, tires1);
    car.drive().should.equal('DI car with engine engine 2 cylinders and brand 1 tires.');
  });
  it('Test engine 2 Tyre Brand 2'.function () {
    const car = new Car(engine2, tires2);
    car.drive().should.equal('DI car with engine engine 2 cylinders and brand 2 tires.');
  });
    
})
Copy the code

How cool is that? The idea of automatic testing is that you configure all the code for all the situations, and all the situations can be tested in one run.

At this point, if you understand the above, you should understand the idea of DI and why DI is used.