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!