什么是依赖?#
每个软件,都是由很多「组件」构成的。这里的「组件」是指广义的组件 ——
组成部件,它可能是函数,可能是类,可能是包,也可能是微服务。软件的架构,就是组件以及组件之间的关系。而这些组件之间的关系,就是(广义的)依赖关系。
依赖有多重要?#
软件的维护工作,本质上都是由「变化」引起的,只要软件还活着,我们就无法对抗变化,只能顺应它。而组件之间的依赖关系决定了变化的传导范围。
一般来说,当被依赖的组件变化时,其依赖者也会随之变化。软件开发最怕的就是牵一发而动全身。所幸,并不是每次变化都必然会传导给它的依赖者们。
对于具体实现细节的修改,只要没有改变其外部契约(可简单理解为接口),其依赖者就不需要修改。对于更大规模的修改,比如更换计费策略,我们是不是就无法控制其传播了?也不见得。只要我们的设计能让两者的接口保持一致,就可以把变化控制在尽可能小的范围内。
整洁架构与 IoC#
对于减少变更传播的方式,Bob 大叔在《架构整洁之道》中做过系统性的阐述。其核心思想是:依赖关系的方向,应该指向更稳定、更核心的业务逻辑。

在这张图中,所有的依赖箭头都指向了中心的「业务实体」。这意味着,无论是 UI、数据库还是外部框架,它们的变更,都不应该影响到核心的业务规则。
这张图看起来很美,却和我们传统的编程方式截然不同。在传统的编程方式下,UI 层的组件需要 new 一个业务服务类的实例,这就产生了一个从
UI 指向业务服务的依赖。但从架构重要性上说,业务服务比 UI 更核心,我们希望的是业务服务能「控制」 UI,而不是被 UI 依赖。
这种「控制」 方向与「依赖」 方向相反的理念,我们称之为「控制反转」 (Inversion of Control, IoC)。
为什么要用依赖注入来管理依赖?#
那么,如何实现「控制反转」呢?答案就是依赖注入(Dependency Injection, DI)。
DI 的核心思想非常朴素:一个组件不应该自己创建它所需要的东西,而应该通过外部「喂」给它。
这个「外部」,就是 Angular 框架为我们提供的 DI 体系。它就像一个无所不能的「大管家」或「三军总后勤部」。

-
它知道如何创建应用中所有需要的「物资」(服务实例)。
-
它也知道每一个「作战单元」(组件)需要哪些「物资」。
-
当一个组件被创建时,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提供者,我们可以根据配置或用户权限,动态地注入一个「完整功能」的服务,或一个「空实现」的服务,轻松实现功能开关(FeatureFlag)。
结语#
依赖注入,在 Angular 中不仅仅是一个方便获取服务实例的工具,它是一种贯穿始终的核心设计哲学。它通过「控制反转」,实现了组件与服务之间的彻底解耦,是
Angular 应用可测试性、可维护性和可扩展性的基石。
从传统的构造函数注入,到现代的 inject() 函数,DI 的「形」在不断进化,但其追求「秩序」与「解耦」的「神」从未改变。掌握它,你才能真正领悟
Angular 的工程之美。