Open jiayisheji opened 2 years ago
SOLID 中的最后一个字母是 Dependency Inversion Principle。它可以帮助我们解耦软件模块,以便更容易地用另一个模块替换一个模块。依赖注入模式使我们能够遵循这个原理。
在这篇文章中,我们将了解什么是依赖注入,为什么它很有用,何时使用它,哪些工具可以帮助前端开发人员使用这种模式。
我们假设你了解 JavaScript 的基本语法,并熟悉面向对象编程的基本概念,例如类和接口。不过,你不需要详细了解类和接口的TypeScript 语法,因为我们会在这篇文章中用到它。
一般来说,依赖关系的概念依赖于上下文,但为了简单起见,我们将依赖关系称为模块所使用的任何模块。当我们开始在代码中使用一个模块时,这个模块就变成了一个依赖项。
我们使用函数参数来模拟依赖。这样,无需深入学术定义,我们可以将依赖关系与函数参数进行比较。两者都以某种方式使用,两者都会影响依赖于它们的软件的功能和可操作性。
// random 函数在没有 'min' 和 'max' 参数的情况下无法工作 function random(min, max) { if (typeof min === 'undefined' || typeof max === 'undefined') { throw new Error('All arguments are required'); } return Math.random() * (max - min) + min; }
在上面的例子中,random 函数有两个参数:min 和 max。如果我们不通过其中一个,函数将抛出一个错误。我们可以得出结论,这个函数取决于这些参数。
random
min
max
然而,这个函数不仅取决于这两个参数,而且依赖于 Math.Random 函数。这是因为如果 Math.Random 没有定义,random 函数也不能工作,所以 Math.Random 也是一种依赖。
Math.Random
如果我们将它作为参数传递给函数,可以使它更清楚:
function random(min, max, randomSource) { if (typeof min === 'undefined' || typeof max === 'undefined' || typeof randomSource === 'undefined') { throw new Error('All arguments are required'); } return randomSource.random() * (max - min) + min; }
现在很明显,random 函数不仅使用 min 和 max,还有随机数生成器。这类函数将被这样调用:
const randomBetweenTenAndTwenty = random(10, 20, Math);
或者如果我们不想每次都手动传递 Math 作为最后一个参数,我们可以在函数参数声明中使用它作为默认值:
Math
function random(min, max, randomSource = Math) { // ...code } // 调用random函数 const randomBetweenTenAndTwenty = random(10, 20);
这就是基本的依赖注入。当然,它还没有得到”规范“,这是非常原始的,它必须用手完成,但关键的思想是一样的:我们将它工作所需要的一切传递给模块。
”规范“
random 函数示例中的代码更改似乎是不必要的。实际上,为什么我们要把 Math 提取到参数中,并像那样使用它?为什么我们不直接在函数体中使用它呢?有两个原因。
当模块明确声明它需要的所有东西时,这个模块测试起来要简单得多。我们看到需要准备好的需要立即运行测试。我们知道是什么影响了这个模块的功能,如果需要,可以用另一个实现替换它,甚至是假实现来替换它。
看起来像依赖性的对象,但是做不同的东西被称为 Mock 对象。当运行测试时,它们可能会跟踪某个函数被调用了多少次,模块的状态是如何改变的,这样以后我们就可以检验预期的结果了。
Mock
一般来说,他们使测试模块更简单,有时它们是测试模块的唯一方法。random 函数是这种情况,我们不能检查这个函数应该返回的最终结果,因为每次调用这个函数都是不同的。然而,我们可以检查这个函数如何使用它的依赖项并从中得出结果。
// 我们可以创建一个Mock对象,它将总是返回0.1而不是一个随机数: const fakeRandomSource = { random: () => 0.1, } // 然后,我们将调用函数,并将这个Mock对象作为依赖项而不是Math: const randomBetweenTenAndTwenty = random(10, 20, fakeRandomSource); // 既然函数的算法是确定的并且不变, 我们可以预期结果总是一样的: randomBetweenTenAndTwenty === 11; // true
在测试时替换依赖项只是一种特殊情况。通常,我们可能出于任何原因想要用另一个模块替换一个模块。如果一个新模块的行为与前一个模块相同,我们可以在没有任何问题的情况下做到这一点:
// 如果一个新对象包含 `random` 方法,我们可以把它当作一种依赖。 const otherRandomSource = { random() { // 自定义随机数生成的实现。 } } const randomNumber = random(10, 20, otherRandomSource);
当我们想让我们的模块尽可能地彼此分开时,这是非常方便的。然而,是否有一种方法可以保证新模块包含 random 方法?(这是至关重要的,因为我们以后在函数随机中依赖这个方法)显然是有的,我们可以通过接口来实现。
接口是一种功能契约。它限制了模块的行为,它必须做什么,以及它不应该做什么。在我们的案例中,为了保证随机方法的存在,我们可以使用接口。
为了确定模块应该有一个返回数字的 random 方法,我们定义了一个接口:
interface RandomSource { random(): number; }
为了确定一个具体的对象必须有这个方法,我们声明这个对象实现了这个接口:
// 使用冒号声明 // 这个对象实现了一个 “RandomSource” 接口 // 因此,必须以这种接口中描述的方式行事。 const otherRandomSource: RandomSource = { random = () => { // 它必须返回一个数字,否则 TypeScript 编译器会抛出一个错误。 return 42; } }
现在我们可以声明我们的 random 函数只接受一个实现 RandomSource 接口的对象作为最后一个参数:
RandomSource
function random(min: number, max: number, source: RandomSource = Math): number { if (typeof min === 'undefined' || typeof max === 'undefined' || typeof source === 'undefined') { throw new Error('All arguments are required'); } return source.random() * (max - min) + min; }
如果我们现在试图传递一个没有实现 RandomSource 接口的对象,TypeScript 编译器会抛出一个错误。
const randomNumber1 = random(1, 10, Math); // `Math` 包含一个 `random` 方法,没有错误。 const randomNumber2 = random(1, 10); // `Math` 被用作默认参数值,没有错误。 const randomNumber3 = random(1, 10, otherRandomSource); // 没有错误,因为`otherRandomSource`实现`RandomSource`接口。 const otherObject = { otherMethod() {}; }; const randomNumber4 = random(1, 10, otherObject); // 错误,'otherObject' 没有实现所需的接口
乍一看,这似乎有点过分。然而,这可以帮助我们获得很多好处。
当我们预先设计一个系统时,我们倾向于使用抽象的契约。使用这些契约,我们为第三方代码设计我们自己的模块和适配器。这解锁了与其他模块交换的能力,而不改变整个系统,而只是改变一部分。
特别是当模块比上面例子中的模块更复杂时,它就变得非常方便。例如,当一个模块具有内部状态时。
在 TypeScript 中,有很多方法可以创建有状态对象,例如使用闭包或类。在这篇文章中,我们将使用类。
作为一个例子,我们将使用一个计数器。作为一个类,它应该写成这样:
class Counter { private state: number = 0; public increase = (): void => { this.state++; } public decrease = (): void => { this.state--; } get stateOf(): number { return this.state; } }
它的方法为我们提供了一种改变其内部状态的方法:
const counter = new Counter(); counter.stateOf; // 0 counter.increase(); counter.stateOf; // 1 counter.decrease(); counter.stateOf; // 0
当像这样的一些物体取决于其他物品时,它就会得到。让我们假设这个计数器不仅应该保持和更改它的内部状态,而且还应该在每次更改时将它记录到一个控制台中。
class Counter { private state: number = 0; // 添加日志记录方法。 private log = (): void => { console.log(this.state); } public increase = (): void => { // 现在当状态发生变化时… this.state++; this.log(); } public decrease = (): void => { // 现在当状态发生变化时… this.state--; this.log(); } get stateOf(): number { return this.state; } }
在这里,我们看到了与本文开头所看到的相同的问题。计数器不仅使用它的状态,而且还使用另一个模块 console。理想情况下,它还应该是明确的,或者换句话说,注入式的。
console
可以使用 setter 或 constructor 在类中注入一个依赖项。我们使用 constructor 。
setter
constructor
constructor (构造函数)是在创建对象时调用的一种特殊方法。通常在对象初始化时指定要执行的所有操作。
例如,如果我们想在创建对象时将问候信息打印到控制台,我们可以使用下面的代码:
class Counter { constructor() { console.log('Hello world!'); } // ...code. } const counter = new Counter(); // "Hello world!"
使用构造函数,我们还可以注入所有需要的依赖项。
我们想将类以与前面例子中的函数相同的方式处理依赖关系。
因此,我们的类 Counter 使用 Console 对象的 log 方法。这意味着该类期望依赖一个具有 log 方法的对象。它是 Console 对象还是其他对象并不重要,这里唯一的条件是对象有一个 log 方法。
Counter
Console
log
当我们想要限制行为时,我们需要使用接口。因此,Counter 的构造函数应该接受一个对象作为参数,该对象实现了一个带有 log 方法的接口。
interface Logger { log(message: string): void; } class Counter { // 这个私有字段将保留一个引用到 logger 对象 private logger: Logger; constructor(logger: Logger) { // 我们将在初始化时设置 this.logger = logger; } // ...code. } // 或者使用字段自动分配 class Counter { // 在以这种方式写入时,构造函数中的参数将自动分配给`logger`私有字段。 constructor(private logger: Logger) {} // ...code. }
要初始化类实例,我们将使用以下代码:
const counter = new Counter(console);
如果我们想要,比方说,使用 alert 而不是 console,我们会这样改变依赖对象:
// 这就足够确保依赖对象 拥有所有必需的方法,或者实现所需的接口。 const customLogger: Logger = { log(message: string): void { alert(message); } } const counter = new Counter(customLogger);
现在,我们的 Counter 类没有使用任何隐式依赖关系。这很好,但是这种注入不方便。
实际上,我们想让它自动化。有一种方法可以做到这一点,它被称为 DI 容器。
总的来说,DI 容器是只做一件事的模块-它为系统中的其他每个模块提供依赖关系。容器确切地知道模块需要哪些依赖项,并在需要时注入它们。这样我们就解放了其他模块来解决这个问题,然后控制到一个特殊的地方。这是 SOLID 在 SRP 和 DIP 原则中描述的行为。
在实践中,为了使其工作,我们需要另一层抽象接口。(Typescript 有这个概念,Javascript 没有)这里的接口是不同模块之间的链接。
容器知道模块需要什么样的行为,知道哪些模块实现它,当创建一个对象时,它会自动提供对它们的访问。
在伪代码中,它看起来像这样:
// 嘿,容器! // 当你被问到一个实现 `SomeInterface` 的对象时,你应该给访问 `SomeClass` 的一个实例。 container.register(SomeInterface, SomeClass);
尽管这段代码不是真实的,但它离现实并不遥远。
TypeScript 有很棒的工具,它可以做我们上面描述的事情。它们都是使用泛型函数来绑定接口和实现。
当然,在前端有强大框架 Angular,它有核心特性就是依赖注入。在后端也有强大框架 Nest,它有核心特性也是依赖注入。Nest 依赖注入也是参考 Angular 实现。
Angular 爱好者把依赖注入特性从 Angular 的 ReflectiveInjector 中提取出来的,创建一个独立库 injection-js。这意味着它设计得很好,功能齐全,快速、可靠,而且经过了很好的测试。有很多库内部使用 injection-js,最有名当属将库编译为 Angular 包格式 ng-packagr(官方 Angular CLI 的一部分)。
injection-js
在这里使用一个简单的 DI 库,使用此工具的代码如下所示:
import {DIContainer} from '@wessberg/di'; // 创建 DI 容器 const container = new DIContainer(); // 创建注入接口 interface Logger { log(message: string): void; } // 实现注入接口 export class ConsoleLogger implements Logger { public log = (message: LogEntry): void => console.log(message); } // 声明当有模块访问一个实现 `Logger` 接口的对象容器时,它应该返回 `ConsoleLogger` 类的一个实例。 container.registerSingleton<Logger, ConsoleLogger>(); // `<Logger, ConsoleLogger>` 语法是一个泛型函数。它使用类型参数将 `Logger` 类型与 `ConsoleLogger` 类型绑定。 // `Logger` 是一个抽象接口,`ConsoleLogger` 是一个更具体的类。 // 由于 TypeScript 将它们都视为类型,所以我们可以在泛型函数中将它们用作类型参数。
现在,如果我们想访问 Counter 类中的依赖项,我们可以通过编写下面的代码来实现:
class Counter { constructor(private logger: Logger) {} private log = (): void => { this.logger.log(this.state); } // ... code. } container.registerSingleton<Counter>();
最后一行在容器本身中注册 Counter 类。这样容器就知道 Counter 可以从中寻求依赖关系。
首先,我们现在只需改变一行就可以改变整个项目的实现。
例如,如果我们想在每个使用它的地方更改 Logger 实现,只需更改模块注册就足够了:
Logger
// 自定义日志实现 class CustomLogger implements Logger { public log = (message: LogEntry): void => alert(message); } // 替换旧的 `ConsoleLogger` 我们只更改下面一行的注册: container.registerSingleton<Logger, CustomLogger>();
此外,我们不必手动传递依赖项,我们不必再保持依赖项的顺序,因此模块之间的耦合会变得更少。
这个容器的杀手锏是它不使用装饰器(如果喜欢装饰器,可以使用 inverse.js)。类型参数注册使得区分基础结构代码和生产代码更加容易。
单例和临时是对象的生命状态类型。
registerSingleton 只创建一个对象,之后它会传递到每个需要它的地方。registerTransient 每次都会创建一个新对象。
registerSingleton
registerTransient
临时对象用于处理一些独特的场景,比如每次都应该从头创建的网络请求对象。当我们可以使用相同的实例(例如,用于记录日志)时,就使用单例对象。
我写了一个小应用程序,点击时候 alert 提示唯一ID,此外,它每 5 秒在控制台显示一条 Hello world 日志。
Hello world
export class Application { constructor( private dateTimeSource: DateTimeSource, private idGenerator: UuidGenerator, private clickHandler: EventHandler<MouseEvent>, private logger: Logger, private timer: Timer, private env: Window ) {} private greet = (): void => this.logger.log('Hello world!'); private setupTimer = (): void => this.timer.invokeEvery(this.greet, 5000); private registerClicks = (): void => this.clickHandler.on('click', this.handleClick); private handleClick = (e: MouseEvent): void => { const position = [e.pageX, e.pageY]; const datetime = this.dateTimeSource.toString(); const eventId = this.idGenerator.generate(); this.env.alert(`${eventId}, ${datetime}: Mouse was clicked at ${position} `); }; public init = (): void => { this.setupTimer(); this.registerClicks(); }; } container.registerSingleton<Application>();
所有有趣的东西都在类构造函数中。在那里,我们向一个容器请求所有依赖项。
这些是主要模块取决于的依赖关系:
为了访问日期和时间,我们使用 BrowserDateTimeSource,它被注册为 DateTimeSource 的实现。请注意,当我们要求这种依赖时,我们使用了接口,因为接口是所有东西都应该依赖于抽象的关键点。
BrowserDateTimeSource
DateTimeSource
export interface DateTimeSource { source: Date; toString: () => string; valueOf: () => string; } export class BrowserDateTimeSource implements DateTimeSource { get source() { return new Date(); } public toString = (): UtcDateTimeString => this.source.toUTCString(); public valueOf = (): TimeStamp => this.source.getTime(); } container.registerSingleton<DateTimeSource, BrowserDateTimeSource>();
唯一的 ID 生成器是第三方的适配器。注意,我们只在注册适配器时引用这个第三方模块一次。如果我们决定用另一个 UUID 生成器可以随时替换,这是很方便的。
export interface UuidGenerator { generate:() => string; } export class IdGenerator implements UuidGenerator { constructor(private adaptee: ThirdPartyGenerator) {} generate = () => this.adaptee(); } container.registerSingleton<ThirdPartyGenerator>(() => uuid); container.registerSingleton<UuidGenerator, IdGenerator>();
事件处理程序使用通用接口 EventHandler<MouseEvent>。稍后从容器中请求这种依赖关系是很重要的。如果在这个接口中传递另一个类型参数,容器将搜索使用该参数注册的模块。当我们处理类似的对象类型时,这是很方便的。
EventHandler<MouseEvent>
export class ClickHandler implements EventHandler<MouseEvent> { constructor(private env: Window) {} public on = (event: EventKind, callback: EventCallback<MouseEvent>): () => void { this.env.addEventListener(event, callback); return () => { this.env.removeEventListener(event, callback); } } public off = (event: EventKind, callback: EventCallback<MouseEvent>): void => this.env.removeEventListener(event, callback); } container.registerSingleton<EventHandler<MouseEvent>, ClickHandler>();
这个我们已经实现过了:
export class ConsoleLogger implements Logger { public log = (message: LogEntry): void => console.log(message); } container.registerSingleton<Logger, ConsoleLogger>();
模拟一个定时器,在间隔时间内执行回调函数。
export interface Timer { invokeEvery:(fn: (...args: any[]) => void, delay: number) => () => void; } export class BrowserTimer implements Timer { constructor() {} invokeEvery = (fn: (...args: any[]) => void, delay: number) => () => void{ let timer = setInterval(fn, delay); () => { clearInterval(timer); timer = null; } } } container.registerSingleton<Timer, BrowserTimer>();
它们是依赖项的依赖项,例如,ClickHandler 类中的 env 或 IdGenerator 中的 adaptee。
ClickHandler
env
IdGenerator
adaptee
对于容器来说,依赖于什么级别并不重要。容器可以毫无问题地提供所有依赖项。(除非有循环依赖,那是另外一个值得深入探讨的话题)
// 对于 `idgenerator`,我们注册了依赖项,如: container.registerSingleton<ThirdPartyGenerator>(() => nanoid); // 对于“ClickHandler”(需要 `Window`) container.registerSingleton<Window>(() => window);
DI 容器的主要问题是,当使用它时,必须注册那里的所有依赖项。它有时并不像我们想要的那样灵活。
另一个缺点是只能从容器访问入口点,这可能看起来有点脏代码。(不过,对于入口点来说,这是可以接受的)
const app= container.get<Application>(); app.init();
今天就到这里吧,伙计们,玩得开心,祝你好运。
SOLID 中的最后一个字母是 Dependency Inversion Principle。它可以帮助我们解耦软件模块,以便更容易地用另一个模块替换一个模块。依赖注入模式使我们能够遵循这个原理。
在这篇文章中,我们将了解什么是依赖注入,为什么它很有用,何时使用它,哪些工具可以帮助前端开发人员使用这种模式。
储备知识
我们假设你了解 JavaScript 的基本语法,并熟悉面向对象编程的基本概念,例如类和接口。不过,你不需要详细了解类和接口的TypeScript 语法,因为我们会在这篇文章中用到它。
什么是依赖
一般来说,依赖关系的概念依赖于上下文,但为了简单起见,我们将依赖关系称为模块所使用的任何模块。当我们开始在代码中使用一个模块时,这个模块就变成了一个依赖项。
我们使用函数参数来模拟依赖。这样,无需深入学术定义,我们可以将依赖关系与函数参数进行比较。两者都以某种方式使用,两者都会影响依赖于它们的软件的功能和可操作性。
在上面的例子中,
random
函数有两个参数:min
和max
。如果我们不通过其中一个,函数将抛出一个错误。我们可以得出结论,这个函数取决于这些参数。然而,这个函数不仅取决于这两个参数,而且依赖于
Math.Random
函数。这是因为如果Math.Random
没有定义,random
函数也不能工作,所以Math.Random
也是一种依赖。如果我们将它作为参数传递给函数,可以使它更清楚:
现在很明显,
random
函数不仅使用min
和max
,还有随机数生成器。这类函数将被这样调用:或者如果我们不想每次都手动传递
Math
作为最后一个参数,我们可以在函数参数声明中使用它作为默认值:这就是基本的依赖注入。当然,它还没有得到
”规范“
,这是非常原始的,它必须用手完成,但关键的思想是一样的:我们将它工作所需要的一切传递给模块。为什么需要依赖注入
random
函数示例中的代码更改似乎是不必要的。实际上,为什么我们要把Math
提取到参数中,并像那样使用它?为什么我们不直接在函数体中使用它呢?有两个原因。可测试性
当模块明确声明它需要的所有东西时,这个模块测试起来要简单得多。我们看到需要准备好的需要立即运行测试。我们知道是什么影响了这个模块的功能,如果需要,可以用另一个实现替换它,甚至是假实现来替换它。
看起来像依赖性的对象,但是做不同的东西被称为
Mock
对象。当运行测试时,它们可能会跟踪某个函数被调用了多少次,模块的状态是如何改变的,这样以后我们就可以检验预期的结果了。一般来说,他们使测试模块更简单,有时它们是测试模块的唯一方法。
random
函数是这种情况,我们不能检查这个函数应该返回的最终结果,因为每次调用这个函数都是不同的。然而,我们可以检查这个函数如何使用它的依赖项并从中得出结果。可替换性(改变依赖关系的能力)
在测试时替换依赖项只是一种特殊情况。通常,我们可能出于任何原因想要用另一个模块替换一个模块。如果一个新模块的行为与前一个模块相同,我们可以在没有任何问题的情况下做到这一点:
当我们想让我们的模块尽可能地彼此分开时,这是非常方便的。然而,是否有一种方法可以保证新模块包含
random
方法?(这是至关重要的,因为我们以后在函数随机中依赖这个方法)显然是有的,我们可以通过接口来实现。接口
接口是一种功能契约。它限制了模块的行为,它必须做什么,以及它不应该做什么。在我们的案例中,为了保证随机方法的存在,我们可以使用接口。
定义行为
为了确定模块应该有一个返回数字的
random
方法,我们定义了一个接口:为了确定一个具体的对象必须有这个方法,我们声明这个对象实现了这个接口:
现在我们可以声明我们的
random
函数只接受一个实现RandomSource
接口的对象作为最后一个参数:如果我们现在试图传递一个没有实现
RandomSource
接口的对象,TypeScript 编译器会抛出一个错误。依赖抽象
乍一看,这似乎有点过分。然而,这可以帮助我们获得很多好处。
当我们预先设计一个系统时,我们倾向于使用抽象的契约。使用这些契约,我们为第三方代码设计我们自己的模块和适配器。这解锁了与其他模块交换的能力,而不改变整个系统,而只是改变一部分。
特别是当模块比上面例子中的模块更复杂时,它就变得非常方便。例如,当一个模块具有内部状态时。
有状态模块
在 TypeScript 中,有很多方法可以创建有状态对象,例如使用闭包或类。在这篇文章中,我们将使用类。
作为一个例子,我们将使用一个计数器。作为一个类,它应该写成这样:
它的方法为我们提供了一种改变其内部状态的方法:
当像这样的一些物体取决于其他物品时,它就会得到。让我们假设这个计数器不仅应该保持和更改它的内部状态,而且还应该在每次更改时将它记录到一个控制台中。
在这里,我们看到了与本文开头所看到的相同的问题。计数器不仅使用它的状态,而且还使用另一个模块
console
。理想情况下,它还应该是明确的,或者换句话说,注入式的。类中的依赖注入
可以使用
setter
或constructor
在类中注入一个依赖项。我们使用constructor
。constructor
(构造函数)是在创建对象时调用的一种特殊方法。通常在对象初始化时指定要执行的所有操作。例如,如果我们想在创建对象时将问候信息打印到控制台,我们可以使用下面的代码:
使用构造函数,我们还可以注入所有需要的依赖项。
简单注入
我们想将类以与前面例子中的函数相同的方式处理依赖关系。
因此,我们的类
Counter
使用Console
对象的log
方法。这意味着该类期望依赖一个具有log
方法的对象。它是Console
对象还是其他对象并不重要,这里唯一的条件是对象有一个log
方法。当我们想要限制行为时,我们需要使用接口。因此,
Counter
的构造函数应该接受一个对象作为参数,该对象实现了一个带有log
方法的接口。要初始化类实例,我们将使用以下代码:
如果我们想要,比方说,使用 alert 而不是 console,我们会这样改变依赖对象:
自动注入和 DI 容器
现在,我们的
Counter
类没有使用任何隐式依赖关系。这很好,但是这种注入不方便。实际上,我们想让它自动化。有一种方法可以做到这一点,它被称为 DI 容器。
总的来说,DI 容器是只做一件事的模块-它为系统中的其他每个模块提供依赖关系。容器确切地知道模块需要哪些依赖项,并在需要时注入它们。这样我们就解放了其他模块来解决这个问题,然后控制到一个特殊的地方。这是 SOLID 在 SRP 和 DIP 原则中描述的行为。
在实践中,为了使其工作,我们需要另一层抽象接口。(Typescript 有这个概念,Javascript 没有)这里的接口是不同模块之间的链接。
容器知道模块需要什么样的行为,知道哪些模块实现它,当创建一个对象时,它会自动提供对它们的访问。
在伪代码中,它看起来像这样:
尽管这段代码不是真实的,但它离现实并不遥远。
自动注入工具
TypeScript 有很棒的工具,它可以做我们上面描述的事情。它们都是使用泛型函数来绑定接口和实现。
当然,在前端有强大框架 Angular,它有核心特性就是依赖注入。在后端也有强大框架 Nest,它有核心特性也是依赖注入。Nest 依赖注入也是参考 Angular 实现。
Angular 爱好者把依赖注入特性从 Angular 的 ReflectiveInjector 中提取出来的,创建一个独立库 injection-js。这意味着它设计得很好,功能齐全,快速、可靠,而且经过了很好的测试。有很多库内部使用
injection-js
,最有名当属将库编译为 Angular 包格式 ng-packagr(官方 Angular CLI 的一部分)。在这里使用一个简单的 DI 库,使用此工具的代码如下所示:
现在,如果我们想访问
Counter
类中的依赖项,我们可以通过编写下面的代码来实现:最后一行在容器本身中注册
Counter
类。这样容器就知道Counter
可以从中寻求依赖关系。使用容器的目的
首先,我们现在只需改变一行就可以改变整个项目的实现。
例如,如果我们想在每个使用它的地方更改
Logger
实现,只需更改模块注册就足够了:此外,我们不必手动传递依赖项,我们不必再保持依赖项的顺序,因此模块之间的耦合会变得更少。
这个容器的杀手锏是它不使用装饰器(如果喜欢装饰器,可以使用 inverse.js)。类型参数注册使得区分基础结构代码和生产代码更加容易。
什么是 registerSingleton
单例和临时是对象的生命状态类型。
registerSingleton
只创建一个对象,之后它会传递到每个需要它的地方。registerTransient
每次都会创建一个新对象。临时对象用于处理一些独特的场景,比如每次都应该从头创建的网络请求对象。当我们可以使用相同的实例(例如,用于记录日志)时,就使用单例对象。
最后的示例
我写了一个小应用程序,点击时候 alert 提示唯一ID,此外,它每 5 秒在控制台显示一条
Hello world
日志。入口点
所有有趣的东西都在类构造函数中。在那里,我们向一个容器请求所有依赖项。
一级依赖项
这些是主要模块取决于的依赖关系:
DateTimeSource
为了访问日期和时间,我们使用
BrowserDateTimeSource
,它被注册为DateTimeSource
的实现。请注意,当我们要求这种依赖时,我们使用了接口,因为接口是所有东西都应该依赖于抽象的关键点。UuidGenerator
唯一的 ID 生成器是第三方的适配器。注意,我们只在注册适配器时引用这个第三方模块一次。如果我们决定用另一个 UUID 生成器可以随时替换,这是很方便的。
EventHandler
事件处理程序使用通用接口
EventHandler<MouseEvent>
。稍后从容器中请求这种依赖关系是很重要的。如果在这个接口中传递另一个类型参数,容器将搜索使用该参数注册的模块。当我们处理类似的对象类型时,这是很方便的。Logger
这个我们已经实现过了:
Timer
模拟一个定时器,在间隔时间内执行回调函数。
二级依赖项
它们是依赖项的依赖项,例如,
ClickHandler
类中的env
或IdGenerator
中的adaptee
。对于容器来说,依赖于什么级别并不重要。容器可以毫无问题地提供所有依赖项。(除非有循环依赖,那是另外一个值得深入探讨的话题)
缺陷
DI 容器的主要问题是,当使用它时,必须注册那里的所有依赖项。它有时并不像我们想要的那样灵活。
另一个缺点是只能从容器访问入口点,这可能看起来有点脏代码。(不过,对于入口点来说,这是可以接受的)
今天就到这里吧,伙计们,玩得开心,祝你好运。