如何在TypeScript中管理依赖关系的边界(附代码)

289 阅读6分钟

当从事具有相当规模的项目时,开发人员倾向于遵循某些有助于管理复杂性的原则,即架构,使应用程序更容易理解和扩展。虽然有无穷无尽的管理架构的方法,但一些流行的例子包括模型视图控制器(MVC)六边形架构模式。

在这些模式中,抽象被设定为高层次的系统设计或架构蓝图,描述了每个模块的职责以及它们之间的关系和依赖关系。正确的架构选择将取决于系统的背景、要求,以及你是否需要实时数据处理或单体网络应用。

让日常开发与架构蓝图保持一致可能是一个挑战,特别是当你的项目或组织正在快速增长时。虽然拉动请求审查、指导、文档和知识共享可能会有所帮助,但仅仅这些可能还不够。

在这篇文章中,我们将讨论TypeScript背景下的依赖关系的重要性。我们将回顾依赖关系未被检查时的潜在隐患,并提出一个解决方案,使我们的代码与架构依赖关系保持同步。让我们开始吧!

TypeScript中的依赖关系

在TypeScript中,像functionsobjectsvalues 这样的变量可以使用ES6模块语法在文件之间导入或导出。用export 注释的变量将被导出,可以使用import 语法导入:

// constants.ts
export const USER = "Alain";

// logic.ts
import { USER } from "./constants";
export const greet = (): string => `Hi ${USER}!`;

// ui.ts
import { greet } from "./logic";
const html = `<h1>${greet()}</h1>`; // <h1>Hi Alain!</h1>

有了这个功能,你可以把应用程序的功能分解成模块,你可以按照架构蓝图来组织这些模块。值得注意的是,导入本地文件和本地或远程软件包是可能的,就像通过npm提供的那样。

这种模块语法允许极大的灵活性,对你可以导入和导出的内容没有任何限制。依赖关系图在整个应用中是隐式定义的:

Dependency Graph Typescript Example

上面例子的依赖关系图

然而,随着项目的发展,这种隐含的依赖关系图可能会不加检查地增长,造成一些问题。

未检查的依赖性管理的缺点

未检查的依赖关系的缺陷之一是,任何程序模块都可以导入并创建对代码库中导出的任何方法的依赖关系。私有方法和辅助方法可以在其模块之外被引用,所以保持一个模块的公共API需要持续的人工监督。

导入第三方软件包带来了另一种权衡。第三方模块很好,可以提高你的开发速度,防止你重新发明车轮。然而,从另一个角度看,过多的依赖关系会使一个项目由于过时的包、包之间的冲突和巨大的捆绑规模而暴露在安全问题中。

第三个也是最主要的问题是,没有办法以编程方式强制或验证代码是否遵循架构的依赖性规则。随着时间的推移,蓝图和实现会逐渐分离,以至于参考架构不再有效,结果使架构的内在特性失效。

例如,在MVC中,我们会失去视图和控制器之间的分离,而控制器中包含了业务逻辑,这使得测试变得困难,并降低了在不破坏业务逻辑的情况下迭代UI的能力。

在下一节中,我们将学习如何使我们的依赖关系显性化,从而使模块内部保持私有,第三方的依赖关系得到控制,并且架构与代码保持同步。

用栅栏添加显式依赖关系

为了使模块之间的依赖关系明确,并设置限制性的依赖规则,我们将使用good-fences包。good-fences使你能够在TypeScript项目中创建和执行边界,它可以大大帮助减轻上述陷阱。

让我们通过一个例子来学习如何使用good-fences包。我们将使用good-fences提供的栅栏概念,以确保项目的实施与计划中的依赖关系图相匹配并长期保持。

栅栏定义了一个模块如何与其他模块和栅栏目录交互。我们可以通过在TypeScript目录中添加一个fence.json 文件来创建一个栅栏。栅栏只限制通过它们的内容,如导入、导出和外部依赖。在一个有栅栏的目录中,没有模块的导入限制。你也可以标记栅栏,以便其他栅栏配置可以标记它们。

依赖关系的界限。一个实际的例子

这个例子的完整代码可以在下面的 repo 中找到。我们将使用一个简单的React应用,它遵循商店驱动的UI架构,类似于React的呈现组件模式。该应用提供了斐波那契佩尔数列的第n个数字的计算。正如我所说,这是一个简单的应用程序。

UI不能访问应用程序中的业务逻辑方法,因为它们被抽象到了商店后面。此外,业务逻辑代码并不依赖于任何UI代码,所以UI可以在不触及业务逻辑的情况下发展。

下面是模块之间的依赖关系图。请注意,模块之间的依赖关系是用箭头标记的。内部模块的颜色是灰色的,而外部包是蓝色的。

Dependency Graph Between Modules

为了实现上述模式,我们将创建三个不同的围栏目录,mathstore ,和ui 。每个目录映射到模式中的一个模块。

为了防止其他模块或其中一个模块的类型进入实现细节,每个围栏目录只允许从index.ts 文件导入。只要不修改index.ts 文件上定义的公共API,实现细节和辅助工具就可以安全地改变。

此外,为了防止循环或不需要的依赖,比如ui 直接依赖于logic ,每个栅栏都有标签,定义了它可以从哪些栅栏导入。

最后,为了缓解未被选中的第三方导入问题,每个栅栏将明确声明哪些第三方软件包允许导入。要添加新的包,你必须修改fence.json 文件以明确这些依赖关系。

我们项目的栅栏配置如下:

// ./math/fence.json
{
  "tags": ["math-module"],
  "exports": ["index"],
  "imports": [],
  "dependencies": []
}
// ./store/fence.json
{
  "tags": ["store-module"],
  "exports": ["index"],
  "imports": ["math-module"],
  "dependencies": ["react-redux", "@reduxjs/toolkit"]
}
// ./ui/fence.json
{
  "tags": ["ui-module"],
  "imports": ["store-module"],
  "dependencies": ["react"]
}

关于栅栏配置选项的深入解释,你可以查看官方文档

所有这些规则都可以通过运行good-fences npm包,指向项目的tsconfig.json 文件,即yarn good-fences ,以编程方式检查。现在你可以把检查作为你的CI/CD管道的一部分,或者作为提交钩子来运行!

结论

正确的依赖性管理和在实施过程中遵循架构设计是一个健康和可维护代码库的重要方面。

good-fences并不是解决这一复杂问题的银弹,而是手边的一个伟大的工具。随着项目的发展,很容易实现人工依赖规则检查的自动化,从而鼓励团队对依赖关系进行有意的处理。代码可以在下面的repo中找到,请随意修改和进一步探索。编码愉快!