系列文章:
1. 共享的API客户端
API请求配置不要使用硬编码,应该在所有请求之间共享基础配置,实现一个单独的API层,这样的好处在于
- 更容易重构,例如更改全局API地址
- 基础配置共享、权限验证等
- 便于实现API与UI层的分离
2. 单独的API层与数据转换
UI层应该与数据请求进行分离,尽量不要混合在一起,对UI组件来说,数据的获取方式并不重要,不管数据是GET请求还是POST请求获取,组件应该只关系传入的 Props 数据。
UI组件应该有自己的数据 Schema定义,这个定义不应该与API接口有关系。
对于API接口,应该进行统一的数据转换、清洗,这一步骤应该放在API层,不要在代码中每个使用API 的地方单独处理,例如
这种情况
理想的方式是在 API层的数据获取函数中进行数据转换处理
3. 领域实体与数据传输对象
UI组件不应该与服务器API耦合在一起,UI层需要有自己的实体对象,而服务器层的API DTO 数据模型应该只在API层使用,例如 API 的 DTO模型是这样的
这个 DTO 目前只是一个简单的二层嵌套对象,但是有可能会变成三层甚至四层,UI组件中不应该这样使用
将 DTO 模型暴露给 UI组件会增加不必要的复杂性,通过多层的对象结构来访问数据,会使代码库变得膨胀、混乱和难以维护。
因此需要定义前端 UI 组件所需要的实体对象
这个实体对象面向前端,在 API 层,将服务端 DTO对象转换为前端实体对象,这样代码逻辑会更清晰
4. 基础设施服务与依赖注入
API 层一个功能请求函数:
在组件中的使用方式
这段代码存在的问题:
- 非单一职责函数:
uploadImage
函数中包含了 数据转换与API请求
- 不容易测试,因为必须模拟一个 APIClient 或者设置一个模拟服务器
优化方案:使用 Service 和依赖注入
- 将
uploadImage
函数分离为 Service
和 API 客户端
- 将其封装在单例类中
- 使用依赖注提高可测试性
分离 Service 和 API
API: uploadImage
仅用于 API 请求。
Service: 专注与数据转换
依赖注入和控制反转
实现反转控制:将服务函数 saveImage
包转在一个类汇总,并让它在构造函数中接收 MediaAPI
作为参数, 关键点在于 Service 将 API 实例存储为私有变量,然后在 saveImage
函数中使用这个变量来调用 uploadImage
函数。
MediaApi interface:
MediaService:
这样实现当我们在进行单元测试时,不需要任何的模拟服务,只需要创建一个简单的 MediaApi 接口的模拟实现,并将其传递给 MediaService 可以通过依赖注入实现一个不错的小单元测试
5. 业务逻辑分离
数据验证和对协调对服务或者 Rest API 的调用,这就是业务逻辑,可以与 UI 框架隔离开
优化这种方式点依然在于 提取业务逻辑并使用依赖注入
第一步:首先将业务逻辑提取到单独的文件中,这个文件的代码不应该包含与 UI 或者 React相关的代码,即使后续需要迁移到另外一个UI框架,这里的代码也可以在不做任何更改的情况下迁移
第二步:用于依赖注入的自定义钩子
通过依赖注入的方式将 replyToShout 函数所依赖的服务抽离出去,作为参数传递进来,这样的好处在于
- 将业务逻辑与UI界面分离,使其独立于用户界面框架
- 使用依赖注入使得测试逻辑变得简单
- 从组件中移除逻辑使它们能够专注于自己的职责: 用户界面
- 此外,可以将业务逻辑与工具函数和自定义钩子分开
6. 将逻辑提取到领域层
领域逻辑是对领域模型(如用户对象)进行操作的代码,听起来有些抽象,他的目标是讲这些逻辑与我们的 UI组件隔离,将其移动到代码库的特定位置,并进行单元测试
案例一
问题代码
存在的问题:这段代码中对 User 和 Image 实体进行了操作,具体来说,这段代码定义了我们应用程序中用户和图像查找的工作方式。
此外, Shout 可能没有图像,因此我们需要一个三元表达式,这不是最优美或者可读的代码
第一步:在领域层创建函数
通过将两个参数设为可选,使函数比所需的更灵活, 同样对图像做类似的事情
在组件中使用
提取领域逻辑的利弊
优势
- 较少的工具函数:没有领域层时,通常不清楚将上述逻辑放在哪里。这些逻辑通常分散在各个组件中,或者可以在工具文件中找到。然而,工具文件可能会变得问题重重,因为它们很容易变成各种共享代码的倾倒场所。
- 可读性:虽然这不是很多,
users.find(({ id }) => id === userId)
需要一些认知负担来将其转换为 ID 查找。相反,阅读 getUserById(users, userId)
更具描述性。如果你有很多这样的行聚集在一起(例如,在组件的顶部),这尤其有效。
- 可测试性:你通常会发现使用 if/switch 语句或三元运算符的代码。每一个这些都意味着需要覆盖多个测试分支。为所有边界情况编写单元测试可能会容易得多,并将集成测试的数量减少到严格必要的测试。
- 可重用性:这些小逻辑片段往往看起来不值得提取到单独的函数中。于是它们在代码中被无限重复。但需求的小变化很容易导致更大的重构。
缺点
- 并不是每个开发者都习惯于以不同的逻辑思考。因此,如果您希望保持代码库的一致性,可能需要文档和培训。
- 开销:正如您在上面的一个示例中看到的,我们使某些域函数比特定组件所需的更灵活(在这里,我们将
users
和userId
参数的getUserById
函数设为可选)。由于我们还通过单元测试覆盖了这些情况,因此引入了比所需更多的代码,导致维护工作量增加。同时,如果您突然遇到来自服务器的意外响应数据,这可以为您节省麻烦。