依赖反转/注入与 IOC 容器 inversify

一、概念

先看看概念释义(百度)- 依赖反转原则:

在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

我第一个想法就是反转什么?再看看释义(chatgpt):

“反转” 指的是将依赖关系从具体实现转向抽象,这样高层模块就不会直接依赖低层模块的实现细节,而是依赖于抽象接口或抽象类。依赖注入则是实现这一反转原则的一个具体手段,通过将依赖项注入到类中,减少了类与其依赖项之间的耦合。

结合给的例子,虽然是 java 的例子,不过大同小异。

传统实现:

public class UserService {
    private UserRepository userRepository;
    public UserService() {
        this.userRepository = new UserRepository(); // 依赖于具体实现
    }
    public void someServiceMethod() {
        userRepository.someMethod();
    }
}

应用依赖反转原则的实现:

// 定义抽象
public interface UserRepository {
    void someMethod();
}
// 实现具体细节
public class UserRepositoryImpl implements UserRepository {
    public void someMethod() {
        // 实现细节
    }
}
// 高层模块依赖于抽象
public class UserService {
    private UserRepository userRepository;
    // 依赖注入
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    public void someServiceMethod() {
        userRepository.someMethod();
    }
}

那么依赖注入又是什么,如果接触过 angular 的话,应该会知道这个概念。释义(chatgpt):

依赖注入是一种实现依赖反转的方法,通过将依赖项(如 UserRepository)注入到 UserService 中,而不是由 UserService 自行创建依赖项。依赖注入可以通过多种方式实现,例如:

  • 构造器注入:通过构造函数将依赖项注入类。
  • 属性注入:通过类的属性(setter 方法)注入依赖项。
  • 接口注入:通过实现特定接口注入依赖项(这种方式在实践中不太常见)。

个人理解:依赖反转是一种编程思想,而依赖注入是其原则的一种实现方式。反转了对依赖的使用。

  • 传统:直接在类里面声明使用一个依赖。如果需求上要改动依赖,就得改动调用方,或者重写一个新的调用方,去调用新的一个依赖。
  • 依赖反转:给调用方传递一个依赖,大家约定好依赖要实现某个接口(方法),调用方就直接用。如此一来,如果需要更改依赖方法,就传递一个新的依赖进去就好。调用方的代码就不用改。
    比如:跑自动化测试和正式使用,就可以给某些功能注入不同的 service,来达到适配不同环境。

个人觉得叫依赖注入还好理解点。不过一个是指导思想,一个是实施方案嘛。

二、IoC 容器

释义:

IoC(Inversion of Control,控制反转)容器是一个用于实现依赖注入的框架。它负责管理对象的创建和依赖关系的注入,从而实现依赖反转原则。IoC 容器自动将依赖项注入到对象中,简化了对象的管理和配置。

其中,inversify 就是前端的 IoC 容器,也有大佬给的中文文档
用得也不多,记录下一些用法,大概相当于我使用的 nginx 冰山一角之于 nginx 全家桶吧。:)

官网示例:

import "reflect-metadata";
import { Container, injectable } from "inversify";

@injectable()
class Katana {
    public hit() {
        return "cut!";
    }
}

@injectable()
class Shuriken {
    public throw() {
        return "hit!";
    }
}

@injectable()
class Ninja implements Ninja {

    private _katana: Katana;
    private _shuriken: Shuriken;
    private id: number;

    public constructor(katana: Katana, shuriken: Shuriken) {
        this.id = 1;
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };
    public run() { this.id += 1; return this.id; };
}

// 容器默认是 inTransientScope { defaultScope: 'transient' }
var container = new Container({ autoBindInjectable: true });
// inTransientScope:每次解析时创建一个新实例,适合无状态服务或需要完全独立的服务。
// inSingletonScope:应用生命周期内只有一个实例,适合全局共享的服务。
// inRequestScope:每个请求创建一个新实例,但在同一请求内共享实例,适合请求范围内共享状态的服务。
container.bind(Ninja).toSelf().inTransientScope();
container.bind(Katana).toSelf();
container.bind(Shuriken).toSelf();

const ninja = container.get(Ninja);
console.log(ninja.run());
console.log(ninja.fight())
console.log(ninja.sneak())

// get 的时候就会创建实例,为了避免太看不出区别,这里延迟获取
// 可以看出不同模式下 id 不一样
setTimeout(() => {
  const ninja1 = container.get(Ninja);
  console.log(ninja1.run())
}, 1000)

一些使用要点

  1. 关于 reflect-metadata,可以看看大佬的文章,Reflect
  2. 装饰器 injectable,结合 reflect,可以简单理解,就是挂载一些元数据在对象上面。我理解也像是把这个抽象类给”注册“了吧。
  3. Container 就是声明一个池子,和要”注册“过的抽象类声明绑定一下。在构造器参数里引入这些抽象类,就会变成是 new 一个实例来使用。
  4. new Container({ autoBindInjectable: true }),有了这个可以不显性 bind,调用的 get 方法的时候就可以把 inject 过的抽象类里 new 一个实例,并且自动 bind 上。

其他问题

我们项目用的是 react + vite,如果构造器在引用参数的时候,是像上面代码那样引入,没有用 inject 装饰的话,会报一个错误:Missing required @inject or @multiInject annotation in: argument 0 in class Ninja.
后面试了传统的 webpack 也会,就是说:

// 简写会报错
class Ninja implements Ninja {
  public constructor(katana: Katana, shuriken: Shuriken) {}
}
// 改成用 inject 装饰器就可以
class Ninja implements Ninja {
  public constructor(@inject(Katana) katana: Katana, @inject(Shuriken) shuriken: Shuriken) {}
}

但是官网给的例子就是有不用属性装饰器的,而且用 ts-node 去启动也没有报错。所以可能是 esbuild、babel 对 typescript 装饰器什么的支持不好。这里可以改成 swc 去处理打包,webpack 的没研究了,vite 的话可以用这个插件,unplugin-swc,如示例用法就行:

import swc from 'unplugin-swc';
export default {
  plugins: [swc.vite()],
};