普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月27日首页

几种依赖注入的使用场景 - InversifyJS为例

作者 irises
2026年1月27日 11:12

依赖注入不仅仅是一个让代码看起来“高级”的工具,它的核心价值在于解耦。通过将对象的“创建权”从业务逻辑中剥离并交给容器,我们能获得极高的灵活性。

关于依赖注入相关概念可参考依赖注入的艺术:编写可扩展 JavaScript 代码的秘密

以下是依赖注入最具代表性的五个使用场景。


1. 单元测试 (Unit Testing)

痛点: 当业务类直接 new 依赖时,测试该类就必须执行依赖的真实逻辑(如真实扣款、真实写库),导致测试缓慢且危险。

❌ 方式 A:不使用 InversifyJS (强耦合)

OrderService 内部强行依赖了 RealPayment。要测试 checkout 方法,你必须真的发起支付,无法轻松 Mock。

TypeScript

// 具体的支付实现
class RealPayment {
    pay(amount: number) {
        console.log(`$$$ 调用银行接口扣款: ${amount}`); // 真实副作用
    }
}

class OrderService {
    private payment: RealPayment;

    constructor() {
        // 😱 致命缺陷:硬编码依赖,测试时无法替换!
        this.payment = new RealPayment();
    }

    checkout(amount: number) {
        this.payment.pay(amount);
    }
}

✅ 方式 B:使用 InversifyJS (依赖抽象)

业务类只依赖接口 IPayment。在单元测试中,我们可以通过容器绑定一个 MockPayment,轻松隔离副作用。

TypeScript

// 1. 定义接口
interface IPayment { pay(amount: number): void; }

// 2. 业务逻辑 (只依赖接口)
@injectable()
class OrderService {
    constructor(
        @inject(TYPES.Payment) private payment: IPayment // 注入接口
    ) {}

    checkout(amount: number) {
        this.payment.pay(amount);
    }
}

// --- 单元测试文件 spec.ts ---
const testContainer = new Container();

// 🧪 测试时:绑定 Mock 实现
const mockPayment = { 
    pay: jest.fn() // 使用 Jest 等测试库的 Mock 函数
}; 
testContainer.bind(TYPES.Payment).toConstantValue(mockPayment);
testContainer.bind(OrderService).toSelf();

const service = testContainer.get(OrderService);
service.checkout(100);

// 断言:验证是否调用了 mock 方法,而不是真的扣款
expect(mockPayment.pay).toHaveBeenCalledWith(100);

2. 可替换的组件 (Swappable Components)

痛点: 同一个接口有多种实现(例如:存储策略既有本地存储,又有云存储)。传统写法通常伴随着大量的 if-else 或工厂模式代码。

❌ 方式 A:不使用 InversifyJS (工厂模式/条件判断)

调用者需要知道具体的实现类,且扩展新策略时需要修改工厂代码。

TypeScript

class LocalStorage { save() { console.log("存硬盘"); } }
class CloudStorage { save() { console.log("存 AWS S3"); } }

class FileManager {
    private storage: any;

    constructor(type: string) {
        // 😱 违反开闭原则:每次加新策略都要改这里
        if (type === 'local') {
            this.storage = new LocalStorage();
        } else {
            this.storage = new CloudStorage();
        }
    }
}

✅ 方式 B:使用 InversifyJS (命名绑定)

使用 @named 标签,可以在不修改业务逻辑代码的情况下,灵活注入不同的策略。

TypeScript

@injectable()
class FileManager {
    constructor(
        // ✨ 优雅:同时注入两种策略,按需使用
        @inject(TYPES.Storage) @named("local") private local: IStorage,
        @inject(TYPES.Storage) @named("cloud") private cloud: IStorage
    ) {}

    backup() {
        this.local.save(); // 先存本地
        this.cloud.save(); // 再存云端
    }
}

// --- 容器配置 ---
container.bind<IStorage>(TYPES.Storage).to(LocalStorage).whenTargetNamed("local");
container.bind<IStorage>(TYPES.Storage).to(CloudStorage).whenTargetNamed("cloud");

3. 跨环境运行 (Cross-Environment Execution)

痛点: 开发环境用 SQLite,生产环境用 PostgreSQL。如果不使用 DI,代码中会充斥着 process.env.NODE_ENV 的判断,导致代码混乱。

❌ 方式 A:不使用 InversifyJS (环境判断污染逻辑)

TypeScript

class DatabaseService {
    constructor() {
        // 😱 环境配置逻辑泄漏到了业务类中
        if (process.env.NODE_ENV === 'production') {
            this.connection = new PostgresConnection();
        } else {
            this.connection = new SqliteConnection();
        }
    }
    
    query() {
        return this.connection.exec("SELECT * FROM users");
    }
}

✅ 方式 B:使用 InversifyJS (容器模块化配置)

业务代码完全干净,环境切换的逻辑被移到了容器配置层(Composition Root)。

TypeScript

// 1. 业务代码 (完全不知道当前是什么环境)
@injectable()
class DatabaseService {
    constructor(@inject(TYPES.DbConnection) private conn: IDbConnection) {}
}

// 2. 环境配置模块 (config.ts)
const devModule = new ContainerModule((bind) => {
    bind<IDbConnection>(TYPES.DbConnection).to(SqliteConnection);
});

const prodModule = new ContainerModule((bind) => {
    bind<IDbConnection>(TYPES.DbConnection).to(PostgresConnection);
});

// 3. 入口文件 (index.ts)
const container = new Container();
if (process.env.NODE_ENV === 'production') {
    container.load(prodModule); // 🏭 加载生产模块
} else {
    container.load(devModule);  // 🛠️ 加载开发模块
}

4. 插件式架构 (Plugin Architecture)

痛点: 系统核心需要加载第三方插件。如果不使用 DI,核心系统必须手动 import 并实例化插件,这使得动态扩展变得极其困难。

❌ 方式 A:不使用 InversifyJS (手动列表)

核心代码必须“认识”每一个插件。

TypeScript

import { GitPlugin } from "./plugins/git";
import { DockerPlugin } from "./plugins/docker";

class App {
    private plugins: any[] = [];

    constructor() {
        // 😱 扩展性差:想加个插件,还得改核心代码的构造函数
        this.plugins.push(new GitPlugin());
        this.plugins.push(new DockerPlugin());
    }

    run() {
        this.plugins.forEach(p => p.exec());
    }
}

✅ 方式 B:使用 InversifyJS (多重注入 Multi-Injection)

核心系统定义接口,插件自行注册到容器。核心系统自动获取所有符合接口的插件。

TypeScript

// 核心系统
@injectable()
class App {
    private plugins: IPlugin[];

    constructor(
        // ✨ 魔法:自动把容器里所有绑定为 TYPES.Plugin 的实例都注入进来,形成数组
        @multiInject(TYPES.Plugin) plugins: IPlugin[]
    ) {
        this.plugins = plugins;
    }
}

// 插件 A (独立文件)
bind<IPlugin>(TYPES.Plugin).to(GitPlugin);

// 插件 B (独立文件)
bind<IPlugin>(TYPES.Plugin).to(DockerPlugin);

// 这种模式下,新增插件只需要 bind 一下,不需要修改 App 类的任何代码。

5. 复杂的生命周期管理 (Singleton vs Transient)

痛点: 某些对象(如缓存、数据库连接池)必须是全局单例,而某些对象(如 HTTP 请求上下文)必须每次新建。手动管理这些单例模式非常容易出错。

❌ 方式 A:不使用 InversifyJS (手动单例模式)

开发者必须手动实现 Singleton 模式,代码啰嗦且难以维护。

TypeScript

class CacheService {
    private static instance: CacheService;
    
    // 😱 样板代码:每个单例类都要写这一坨逻辑
    private constructor() {} 

    public static getInstance(): CacheService {
        if (!CacheService.instance) {
            CacheService.instance = new CacheService();
        }
        return CacheService.instance;
    }
}

// 使用时必须小心
const cache = CacheService.getInstance();

✅ 方式 B:使用 InversifyJS (声明式生命周期)

类本身不需要知道自己是不是单例,全靠容器配置。

TypeScript

@injectable()
class CacheService {
    constructor() { console.log("CacheService Created"); }
}

@injectable()
class RequestHandler {
    constructor() { console.log("RequestHandler Created"); }
}

// --- 容器配置 ---
// 1. 单例:整个应用只创建一次
container.bind(CacheService).toSelf().inSingletonScope();

// 2. 瞬态:每次请求都创建新的
container.bind(RequestHandler).toSelf().inTransientScope();

// --- 运行结果 ---
const cache1 = container.get(CacheService);
const cache2 = container.get(CacheService);
// 输出: "CacheService Created" (只输出一次,cache1 === cache2)

const handler1 = container.get(RequestHandler);
const handler2 = container.get(RequestHandler);
// 输出: "RequestHandler Created" (输出两次,handler1 !== handler2)

❌
❌