An overview of the

Dependency injection (DI) and inversion of control (INVERSION of control) have become essential design principles in larger frameworks at the front end. InversifyJS is the most mature front-end IoC(Inversion of Control) management library.

Basic Concepts of IoC

The high-level module should not depend on the low-level module, but both should depend on its abstraction. Abstraction should not depend on details; Details should depend on abstractions.

For example, 🌰

Class A depends directly on class B. If you want to change class A to depend on class C, you must change the code of class A to do so. In this scenario, class A is typically A high-level module responsible for complex business logic. Classes B and C are low-level modules responsible for basic atomic operations; If class A were modified, it would introduce unnecessary risk to the program. Solution: Change class A to depend on interface I. Class B and C implement interface I respectively. Class A indirectly relates to class B or C through interface I, which greatly reduces the probability of changing class A.

Basic Concepts of DI

In fact, Dependency Injection and IoC come from the same root. The two are originally the same thing, but due to the vague concept of inversion of control (it may only be understood as the level of the container controlling objects, so it is difficult to think of who maintains the object relationship), So in 2004 Martin Fowler, the master figure, gave it a new name: “Dependency injection.” A common representation of class A’s dependence on class B is to use instance of B in A.

For example, 🌰

Note: the official website uses an example of ninjas doing inversify, so I’ll use ninjas for comparison

Dependency injection is not used
interface Weapon {
    name: string;
}
interface Warrior {
    name: string;
    weapon: Weapon;
}
/ / katana
class Katana implements Weapon {
    public name: string;
    public constructor() {
      this.name = "Katana"; }}/ / a ninja
class Ninja implements Warrior {
    public name: string;
    public weapon: Weapon;
    public constructor() {
      this.name = "Ninja";
      const katana = new Katana();
      this.weapon = katana; }}const ninja = new Ninja();
console.log(ninja.weapon.name); // Katana
Copy the code

Ninja has a Katana, so now Ninja relies on Katana, and we use an instance of Katana in the Ninja constructor. There’s nothing wrong with that, but what if Katana changes? For example, if Katana becomes name, you can configure:

class Katana implements Weapon {
    public name: string;
    public constructor(name: string) {
      this.name = name; }}Copy the code

The Ninja component would then report an error:

We’ll have to change the code in Ninja. This is obviously unreasonable and we hope Ninja can just use Katana and not focus on what Katana is actually doing. So we can upload Katana’s instance via Ninja’s constructor.

Using dependency Injection

Just look at the Ninja and call changes:

class Ninja implements Warrior {
    public name: string;
    public weapon: Weapon;
    public constructor(katana: Katana) {
      this.name = "Ninja";
      this.weapon = katana; }}const ninja = new Ninja(new Katana("katana"));
Copy the code

No matter what happens inside Katana, it won’t affect Ninja’s use. 🎉 🎉 🎉

The problem

The benefits of dependency injection are already clear from the above example, so what’s wrong with the above example? If Ninja had Shuriken(sword in hand) in addition to Katana, it would inject instances of Shuriken. Does Shuriken continue to rely on instances of other components? Once such component dependencies are added, the relationship between them becomes difficult to maintain and difficult to troubleshoot.

InversifyJS

Let’s take a look at the official Basic example. Since we’re going to follow a dependency on abstraction rather than implementation, let’s first define some interfaces (aka abstractions)

// file interfaces.ts
export interface Warrior {
    fight(): string;
    sneak(): string;
}
export interface Weapon {
    hit(): string;
}
export interface ThrowableWeapon {
    throw() :string;
}
Copy the code

Inversify requires the use of Type as an identifier. Symbol is recommended to ensure global uniqueness. See ES6 for more information on Symbol usage

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

Next, define some classes to implement the interface above. Note that the @ Injectable decorator should be added to all implementations. When Ninja needs to rely on Katana and Shuriken, we need to use the @Inject decorator for injection

// file entities.ts 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!" ; }} 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

Of course, instead of the constructor, we could inject it into Ninja’s properties:

@injectable(a)class Ninja implements Warrior {
    @inject(TYPES.Weapon) private _katana: Weapon;
    @inject(TYPES.ThrowableWeapon) private _shuriken: ThrowableWeapon;
    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }}Copy the code

Next we need to configure a Container, the recommended name of which is inversify.config.ts. This file is the only place where coupling exists in the entire project and there should be no dependencies elsewhere in the project

// 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();
// Ninja is an implementation of Warrior, so we bound them, as did Katana and Shuriken
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

Finally, let’s look at the results of the run:

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

// Create an instance using the get
      
        method of the Container
      
const ninja = myContainer.get<Warrior>(TYPES.Warrior);
console.log(ninja.fight()); // cut!
console.log(ninja.sneak()); // hit!
Copy the code

As you can see, our Ninja was able to call Shuriken and Katana’s instance methods without injecting them!

Alacrity 🌺

Reference article: Dependency inversion principle InversifyJS

Please do not reprint without permission!