You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// random 函数在没有 'min' 和 'max' 参数的情况下无法工作functionrandom(min,max){if(typeofmin==='undefined'||typeofmax==='undefined'){thrownewError('All arguments are required');}returnMath.random()*(max-min)+min;}
functionrandom(min,max,randomSource){if(typeofmin==='undefined'||typeofmax==='undefined'||typeofrandomSource==='undefined'){thrownewError('All arguments are required');}returnrandomSource.random()*(max-min)+min;}
现在很明显,random 函数不仅使用 min 和 max,还有随机数生成器。这类函数将被这样调用:
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 容器的主要问题是,当使用它时,必须注册那里的所有依赖项。它有时并不像我们想要的那样灵活。
另一个缺点是只能从容器访问入口点,这可能看起来有点脏代码。(不过,对于入口点来说,这是可以接受的)
今天就到这里吧,伙计们,玩得开心,祝你好运。
The text was updated successfully, but these errors were encountered: