什么是依赖?#

每个软件,都是由很多「组件」构成的。这里的「组件」是指广义的组件 ——

组成部件,它可能是函数,可能是类,可能是包,也可能是微服务。软件的架构,就是组件以及组件之间的关系。而这些组件之间的关系,就是(广义的)依赖关系。

依赖有多重要?#

软件的维护工作,本质上都是由「变化」引起的,只要软件还活着,我们就无法对抗变化,只能顺应它。而组件之间的依赖关系决定了变化的传导范围。

一般来说,当被依赖的组件变化时,其依赖者也会随之变化。软件开发最怕的就是牵一发而动全身。所幸,并不是每次变化都必然会传导给它的依赖者们。

对于具体实现细节的修改,只要没有改变其外部契约(可简单理解为接口),其依赖者就不需要修改。对于更大规模的修改,比如更换计费策略,我们是不是就无法控制其传播了?也不见得。只要我们的设计能让两者的接口保持一致,就可以把变化控制在尽可能小的范围内。

整洁架构与 IoC#

对于减少变更传播的方式,Bob 大叔在《架构整洁之道》中做过系统性的阐述。其核心思想是:依赖关系的方向,应该指向更稳定、更核心的业务逻辑。

整洁架构

在这张图中,所有的依赖箭头都指向了中心的「业务实体」。这意味着,无论是 UI、数据库还是外部框架,它们的变更,都不应该影响到核心的业务规则。

这张图看起来很美,却和我们传统的编程方式截然不同。在传统的编程方式下,UI 层的组件需要 new 一个业务服务类的实例,这就产生了一个从

UI 指向业务服务的依赖。但从架构重要性上说,业务服务比 UI 更核心,我们希望的是业务服务能「控制」 UI,而不是被 UI 依赖。

这种「控制」 方向与「依赖」 方向相反的理念,我们称之为「控制反转」 (Inversion of Control, IoC)。

为什么要用依赖注入来管理依赖?#

那么,如何实现「控制反转」呢?答案就是依赖注入(Dependency Injection, DI)

DI 的核心思想非常朴素:一个组件不应该自己创建它所需要的东西,而应该通过外部「喂」给它。

这个「外部」,就是 Angular 框架为我们提供的 DI 体系。它就像一个无所不能的「大管家」或「三军总后勤部」。

文生图:扁平插画风格,一位穿着燕尾服、一丝不苟的“大管家”正微笑着将一份新鲜的龙虾和一把闪亮的菜刀递给一位戴着厨师帽、专注于烹饪的厨师。背景是整洁明亮的现代化厨房,色彩温馨。

  1. 它知道如何创建应用中所有需要的「物资」(服务实例)。

  2. 它也知道每一个「作战单元」(组件)需要哪些「物资」。

  3. 当一个组件被创建时,DI 体系会自动把该组件所声明的依赖,准备好并「注入」给它。

组件只需要过「衣来伸手,饭来张口」的生活,专注于自己的「作战任务」(业务逻辑),而无需关心「后勤补给」(依赖的创建和来源)。这种模式,就是依赖注入。

现代 Angular 中的 DI 实现#

1. 「御赐金牌」:providedIn: 'root'#

在现代 Angular 中,将一个服务注册到 DI 体系中最简单、最推荐的方式,就是在服务的 @Injectable 装饰器中,设置

providedIn: 'root'

@Injectable({
    providedIn: 'root',
})
export class LoggerService {
    log(message: string) {
        console.log(message);
    }
}

这行代码,就像皇帝赐给 LoggerService 一块「金牌」,告诉 DI 体系:

  • 该服务在应用的「根(root)」注入器中提供。

  • 它在整个应用中是单例的,即所有地方注入的都是同一个实例。

  • 它是可摇树优化(Tree-shakeable) 的。如果整个应用没有任何地方用到这个服务,打包时它就会被自动移除,减小应用体积。

2. 「就近补给」:组件级 providers#

独立组件也可以拥有自己的 providers 数组。在组件的 @Component 装饰器中提供服务,会为每一个该组件的实例,都创建一个*

*全新**的服务实例。

@Component({
    standalone: true,
    selector: 'app-counter',
    template: `...`,
    providers: [CounterStateService], // 在此提供服务
})
export class CounterComponent {
    constructor(private state: CounterStateService) {
        // 这里的 state 是 CounterComponent 独有的实例
    }
}

这种方式非常适合那些与特定组件生命周期紧密绑定的、有状态的服务,实现了「就近补给」和「状态隔离」。

3. DI 的新姿势:无处不在的 inject() 函数#

在 Angular v14 之后,inject() 函数的出现,让 DI 的使用变得更加灵活,它将 DI 从「构造函数」的专属,解放了出来。

inject() 允许我们在注入上下文中(如组件的属性初始化器、工厂函数、生命周期钩子等),以函数调用的方式直接获取依赖。

@Component({...})
export class UserProfileComponent {
    // 1. 不再必须写构造函数,直接在属性定义时注入
    private userService = inject(UserService);
    private router = inject(Router);
    user = toSignal(this.userService.getCurrentUser());
    editUser() {
        // 逻辑代码中也可以使用
        const route = inject(ActivatedRoute);
        // ...
    }
}

inject() 函数就像一个「魔法按钮」,只要你在正确的时机(在 DI 体系的管理下)按下它,就能随时召唤出你需要的服务。它极大地推动了

Angular 向更简洁、更函数式的编程风格演进,例如,函数式的路由守卫和拦截器,都依赖于 inject() 的能力。

通过依赖注入改善设计#

深入理解了依赖注入技术,就可以将其灵活运用在你的各项实际工作中,设计出优美而灵活的应用架构。

  • 策略模式:当一个功能有多种实现策略时(如不同的计费算法),我们可以定义一个抽象的 BillingStrategy 接口(或

    InjectionToken),然后通过 DI 在运行时注入具体的策略实现类。更换策略,只需修改一处提供者配置即可。

  • 功能开关:通过 useFactory 提供者,我们可以根据配置或用户权限,动态地注入一个「完整功能」的服务,或一个「空实现」的服务,轻松实现功能开关(Feature

    Flag)。

结语#

依赖注入,在 Angular 中不仅仅是一个方便获取服务实例的工具,它是一种贯穿始终的核心设计哲学。它通过「控制反转」,实现了组件与服务之间的彻底解耦,是

Angular 应用可测试性、可维护性和可扩展性的基石。

从传统的构造函数注入,到现代的 inject() 函数,DI 的「形」在不断进化,但其追求「秩序」与「解耦」的「神」从未改变。掌握它,你才能真正领悟

Angular 的工程之美。