服务架构重构

我在设计之初,想把标题改为容器架构重构,它对应上 C4 模型。纠结之后,我还是改为了服务架构。可见,命名之难。

整洁架构

整洁架构

Robert C. Martin 总结了六边形架构(即端口与适配器架构)、DCI (Data-Context-Interactions,数据-场景-交互)架构、BCI(Boundary Control Entity,Boundary Control Entity)架构等多种架构,归纳出了这些架构的基本特点:

  • 框架无关性。系统不依赖于框架中的某个函数,框架只是一个工具,系统不能适应于框架
  • 可被测试。业务逻辑脱离于 UI、数据库等外部元素进行测试。
  • UI 无关性。不需要修改系统的其它部分,就可以变更 UI,诸如由 Web 界面替换成 CLI。
  • 数据库无关性。业务逻辑与数据库之间需要进行解耦,我们可以随意切换 LocalStroage、IndexedDB、Web SQL。
  • 外部机构(agency)无关性。系统的业务逻辑,不需要知道其它外部接口,诸如安全、调度、代理等。

如图所示 Clean Architecture 一共分为四个环,四个层级。环与环之间,存在一个依赖关系原则:源代码中的依赖关系,必须只指向同心圆的内层,即由低层机制指向高级策略 。其类似于 SOLID 中的依赖倒置原则:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象

与此同时,四个环都存在各自核心的概念:

  • 实体 Entities (又称领域对象或业务对象,实体用于封装企业范围的业务规则)
  • 用例 Use Cases(交互器,用例是特定于应用的业务逻辑)
  • 接口适配器 Interface Adapters (接口适配器层的主要作用是转换数据)
  • 框架和驱动(Frameworks and Drivers),最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等 这个介绍可能有些简单,让我复制/粘贴一下更详细的解释:

实体(Entities) ,实体用于封装企业范围的业务规则。实体可以是拥有方法的对象,也可以是数据结构和函数的集合。如果没有企业,只是单个应用,那么实体就是应用里的业务对象。这些对象封装了最通用和高层的业务规则,极少会受到外部变化的影响。任何操作层面的改动都不会影响到这一层。

用例(Use Cases) ,用例是特定于应用的业务逻辑,一般用来完成用户的某个操作。用例协调数据流向或者流出实体层,并且在此过程中通过执行实体的业务规则来达成用例的目标。用例层的改动不会影响到内部的实体层,同时也不会受外层的改动影响,比如数据库、UI 和框架的变动。只有而且应当应用的操作发生变化的时候,用例层的代码才随之修改。

接口适配器(Interface Adapters) 。接口适配器层的主要作用是转换数据,数据从最适合内部用例层和实体层的结构转换成适合外层(比如数据持久化框架)的结构。反之,来自于外部服务的数据也会在这层转换为内层需要的结构。

框架和驱动(Frameworks and Drivers) 。最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等。通常在这层不需要写太多代码,大多是一些用来跟内层通信的胶水代码。这一层包含了所有实现细节,把实现细节锁定在这一层能够减少它们的改动对整个系统造成的伤害。

设计新架构

根据不同项目的实际情况,在真正落地的时候,会存在一些细微的差距。

如我的同事 @huleTWdjango-ddd-demo 项目中使用的 Python + Django 下的 DDD 分层架构如下所示:

  • apis :放各种 url 的 dispatcher
  • application :放各种 use case,use case 只能访问 domain 里面的 domain service
  • domain :放各个聚合,各个聚合里面有一个 domain service,domain service 操作可以聚合根,包含业务逻辑
  • infrastructure :放读取数据(服务,中间件),以及公共的 util 包

我的同事 @howiehuDDD Architecture Samples 项目中使用了整洁架构分层如下:

├── adapters      适配器
│   ├── inbound   入站适配器
│   └── outbound  出站适配器
├── application   应用层
│   ├── concepts
│   ├── dto
│   ├── gateway
│   └── usecases  用例
└── domain        领域层
    ├── contexts  限界上下文
    └── core      核心概念

因此对于整洁架构的外部适配器,人们通过有多种叫法 interfaces、apis、facades、presentation、adapters 等等,

而诸如 application、domain 和 infrastructure 来说,命名上倒是没有太大的区别。但是在实现上还是有相当大的区别:

粘合层(service) 。在 application 中的粘合层倒是有不同的叫法 service(application service)、usecases、interactors。但是在真正实施的时候,还会有巨大的差别,采用 service 可能会出现上帝类,于是就出现用例驱动的 xxxUsecase。

瘦 application vs 胖 application 。采用 PresentationDomainDataLayering 架构,即瘦 application 层,会把用例放在 domain 中;而采用胖 application 架构,则会把 usecase 放在 application 中。两者的使用场景的区别,主要取决于 CRUD 的纯净度 。如果你有 BFF,那么瘦 application 层适合于你;如果你是单体,又或者是跨实体操作多,那么胖 application 层适合你。

不过,这些都不重要,重要的是要不要出现尖叫架构 —— 一看就懂得业务:

尖叫架构

你只需要回答一个问题,你的场景复杂吗?复杂的话,你就拆 —— 将复杂问题繁杂化。

实施新分层架构

实施新分层架构是一个持续的过程,它需要配合后续的各种重构模式。

划分类,移动代码

这一步我们所做的是:移动旧的 service、controller、model 为竖直 + 水平架构。

对于大单体应用来说,从分层架构上,移到新的架构并不难:按各种层级创建应用,移动代码。对于微服务架构应该来说,这一步也不算麻烦。

技术模块化 => 重搭

对于原先模块划分不合理的应用来说,如:

├── pom.xml
├── zheng-api-common
│   └── pom.xml
├── zheng-api-rpc-api
│   └── pom.xml
├── zheng-api-rpc-service
│   └── pom.xml
└── zheng-api-server
    └── pom.xml

因为包、类之间本身是根据技术维度来划分的,如果我们计划以业务维度重新开发时,便就得创建新的目录结构,再移入新的类包。但是这样做的一个问题是,中间存在一个不可恢复的状态,会在一定程度上影响重构效果。

中间态分层

考虑到服务重构的难度,它会存在着一个长期的中间态分层架构。原因有很多:

  • 无法短期内重构完
  • 未拆到正确位置的 API
  • 领域层中的 repository 未拆分

所以,可以考虑将瘦 application 层作为中间态架构。

验收条件:构建

是的,在这一步里,由于只是移动文件,所以只需要执行一下构建,你就能验证移动文件是否正确。不过,由于有 IDEA 这样的工具上,想必不是问题。然而,要是代码库过于庞大,那么我建议你试试 coca refactor

下一节:创建通用的共享组件导致了一系列问题,比如耦合、协调难度和复杂度增加。