关于重构:整洁架构和商家前端的重构之路

55次阅读

共计 5228 个字符,预计需要花费 14 分钟才能阅读完成。

1. 背景

团队归属于前方业务撑持部门,组内的我的项目都以 pc 中后盾利用为主。比照挪动端利用,代码库比拟宏大,业务逻辑也绝对简单。在继续的迭代过程中,咱们发现以后的代码仓库依然有不少能够优化的点:

能够削弱对 ui 框架的依赖

21 年前端平台决定技术栈对立迁徙到 React 生态,后续平台的根底建设也都围绕 React 开展,这就使得商家应用 Vue 生态做开发的零碎面临技术栈迁徙的难题,将业务逻辑和 UI 框架节藕变得异样重要。

代码格调能够更加对立

随着代码量和团队成员的减少,利用里格调迥异的代码也越来越多。为了可能继续迅速的进行迭代,团队急需一套对立的顶层代码架构设计计划。

能够集成自动化测试用例

随着业务变得越来越简单,在迅速的迭代过程中团队须要频繁地对性能进行回归,因而咱们对于自动化单测用例的诉求也变的越来越强烈。

为了实现以上的优化,四组对现有的利用架构做了一次重构,而重构的外围就是整洁架构。

2. 整洁架构(The Clean Architecture)

整洁架构 (The clean architecture) 是由 Robert C. Martin (Uncle Bob)在 2012 年提出的一套代码组织的理念,其外围次要是根据各局部代码作用的不同将其拆分成不同的档次,在各层次间制订了明确的依赖准则,以达到以下目标:

  1. 与框架无关:无论是前端代码还是服务端代码,其逻辑自身都应该是独立的,不应该依赖于某一个第三方框架或工具库。一套独立的代码能够把第三方框架等作为工具应用。
  2. 可测试:代码中的业务逻辑能够在不依赖 ui、数据库、服务器的状况下进行测试。
  3. 和 ui 无关:代码中的业务逻辑不应该和 ui 做强绑定。比方把一个 web 利用切换成桌面利用,业务逻辑不应该受到影响。
  4. 和数据库无关:无论数据库用的是 mysql 还是 mongodb,无论其怎么变,都不该影响到业务逻辑。
  5. 和内部服务无关:无论内部服务怎么变,都不影响到应用该服务的业务逻辑。

为了实现以上目标,整洁架构把利用划分成了 entities、use cases、interface adapters(MVC、MVP 等)、Web/DB 等至多四层。这套架构除了分层之外,在层与层之间还有一个十分明确的依赖关系,外层的逻辑依赖内层的逻辑。

Entity
entities 封装了企业级的业务逻辑和规定。entities 没有什么固定的模式,无论是一个对象也好,是一堆函数的汇合也好,惟一的规范就是可能被企业的各个利用所复用。

Use Case
entities 封装了企业里最通用的一部分逻辑,而利用各自的业务逻辑就都封装在 use case 外面。日常开发中最常见的对于某个模型的 crud 操作就属于 usecase 这一层。

Interface Adapter
这一层相似于胶水层,须要负责内圈的 entity 和 use case 同外圈的 external interfaces 之间的数据转化。须要把外层服务的数据转化成内层 entity 和 usecase 能够生产的数据,反之亦然。如下面图上画的,这一层有时候可能很简略(一个转化函数),有时候可能简单到蕴含一整个 MVC/MVP 的架构。

External Interfaces
咱们须要依赖的内部服务,第三方框架,以及须要糊的页面 UI 都归属在这一层。这一层齐全不感知内圈的任何逻辑,所以无论这一层怎么变 (ui 变动),都不应该影响到内圈的应用层逻辑(usecase) 和企业级逻辑(entity)。

依赖准则
在整洁架构的原始设计中,并不是强制肯定只能写这么四层,依据业务的须要还能够拆分的更细。不过无论怎么拆,都须要恪守后面提到的从外至内的依赖准则。即 entity 作为企业级的通用逻辑,不能依赖任何模块。而外层的 ui 等则能够应用 usecase、entity。

3. 重构

后面介绍了以后代码库目前的一些具体问题,而整洁架构的理念正好能够帮忙咱们优化代码可维护性。

作为前端,咱们的业务逻辑不应该依赖视图层(ui 框架及其生态),同时该当保障业务逻辑的独立性和可复用性(usecase & entity)。最初,作为数据驱动的端利用,要保障利用视图渲染和业务逻辑等不受数据变动的影响(adapter & entity)。

依据以上的思考,咱们对“整洁架构”做了如下落地。

Entities
对于前端利用来说,在 entity 层咱们只须要将服务端的生数据做一层简略的形象,生成一个贫血对象给后续的渲染和交互逻辑应用。


以上是商家后盾订单模型的 entity 工厂函数,工厂次要负责对服务端返回的生数据进行加工解决,让其满足渲染层和逻辑层的要求。除了形象数据之外,能够看到在 entity 工厂还对数据进行了校验,将脏数据、不合乎预期的数据全副解决掉或者进行兜底(具体操作要看业务场景)。

有一点须要留神的是,在设计 entity 的时候 (尤其是根底 entity) 须要思考复用性。举个例子,在下面 orderEntity 的根底上,咱们通过简略的组合就能够生成一个虚构商品订单 entity:

如此一来,咱们就通过 entity 层达到了 2 个目标:

  1. 把前端的逻辑和服务端接口数据隔离开,无论服务端怎么变,前端后续的渲染、业务代码不须要变,咱们只须要变更 entitiy 工厂函数;并且通过 entity 层解决过后,所有流入后续渲染 & 交互逻辑的数据都是牢靠的;对于局部异样数据,前端利用能够第一工夫发现并报警。
  2. 通过对业务模型进行形象,实现了模块间的组合、复用。另外,形象出的 entity 对代码的维护性也有十分大的帮忙,开发者能够十分直观的晓得所应用的 entity 所蕴含的所有字段。

Usecase
usecase 这一层即是围绕 entity 开展的一系列 crud 操作,以及为了页面渲染做的一些联动(通过 ui store 实现)。因为以后架构的起因(没有 bff 层),usecase 还可能承当局部微服务串联的工作。

举个例子,商家后盾订单页面在渲染前有一堆筹备逻辑:

  1. 依据 route 的 query 参数以及一些商家类型参数来决定默认选中哪个 tab
  2. 依据是国内商家还是境外商家,调用对应的供应商接口来更新供应商下拉框
    当初大抵的实现是:

咱们能看到 7 -15、24-125 行对 this.subType 进行了赋值。但因为咱们无奈确定 20 行的函数是否也对 this.subType 进行了赋值,所以光凭 mounted 函数的代码咱们并不能齐全确定 subType 的值到底是什么,须要跳转到 getAllLogisticsCarrier 函数确认。这段代码在这里曾经做了简化,理论的代码像 getAllLogisticsCarrier 这样的调用还有好几个,要想搞清楚逻辑就得把所有函数全看一遍,代码的可读性个别。同时,因为函数都封装在 ui 组件里,因而要想给函数笼罩单测的话也须要一些革新。
为了解决问题,咱们将这部分逻辑都拆分到 usecase 层:


首先,能够看到所有 usecase 肯定是一个纯函数,不会存在副作用的问题。

其次,prepareOrderPage usecase 专门为订单页定制,拆分后一眼就能看进去订单页的筹备工作须要干决定选中的 tab 和拉取供应商列表两件事件。而另一个拆分进去的 queryLogisticsCarriers 则是封装了商家后盾跨境、国内两种逻辑,后续无论跨境还是国内的逻辑如何变更,其影响范畴被限度在了 queryLogisticsCarriers 函数,咱们须要对其进行性能回归;而对于 prepareOrderPage 来说,queryLogisticsCarriers 只是() => Promise<{ carriers: ICarrires}> 的一个实现而已,其外部调用 queryLogisticsCarriers 的逻辑齐全不受影响,不须要进行回归。

最初,而因为咱们做了依赖倒置,咱们能够非常容易的给 usecase 笼罩单测:

单测除了进行性能回归之外,它的形容 (demo 里应用了 Given-When-Then 的格局,因为篇幅的起因,对于单测的细节在后续的文章再进行介绍) 对于理解代码的逻辑十分十分十分有帮忙。因为单测和代码逻辑强行绑定的缘故,咱们甚至能够将单测形容当成一份实时更新的业务文档。

除了不便写单测之外,在通过 usecase 拆分实现之后,ui 组件真正成为了只负责“ui”和监听用户交互行为的组件,这为咱们后续的 React 技术栈迁徙奠定了根底;通过 usecase 咱们也实现了很不错的模块化,对于应用比拟多的一些 entity,他的 crud 操作能够通过独立的 usecase 具备了在多个页面甚至利用间复用的能力。

Adapter
下面 usecase 例子中的 fetchAllLogisticsCarrier 就是一个 adapter,这一层起到的作用是将内部零碎返回的数据转化成 entity,并以一种对立的数据格式返回回来。

这一层很外围的一点即是能够依赖 entity 的工厂函数,将接口返回的数据转化成前端本人设计的模型数据,保障流入 usecase 和 ui 层的数据都是通过解决的“洁净数据”。除此之外,通常在这一层咱们会用一种固定的数据格式返回数据,比方例子中的 {success: boolean, data?: any}。这样做次要是为了抹平对接多个零碎带来的差异性,同时缩小多人合作时的沟通老本。

通过 Adapter + entity 的组合,咱们根本造成了前端利用和后端服务之间的防腐层,使得前端能够在齐全不分明接口定义的状况下实现 ui 渲染、usecase 等逻辑的开发。在服务端产出定义后,前端只须要将理论接口返回适配到本人定义的模型 (通过 entity) 即可。这一点对前端的测试周提效十分十分十分重要,因为防腐层的存在,咱们能够在测试周实现需要评审之后依据 prd 的内容设计出业务模型,并以此实现需要开发,在真正进入研发周后只须要和服务端对接实现 adapter 这一层的适配即可。

在实际过程中,咱们发现在对接同一个零碎的时候 (对商家来说就是 stark 服务) 各个 adapter 对于异样的解决简直截然不同(上述的 11-15 行),咱们能够通过 Proxy 对其进行抽离实现复用。当然,后续咱们也齐全有机会依据接口定义来主动生成 adapter。

UI
在通过后面的拆分之后,无论咱们的 UI 层用 React 还是 Vue 来写,要做的工作都很简略了:

  1. 监听交互事件并调用对应的 usecase 来进行响应
  2. 通过 usecase 来获取 entity 数据进行渲染

因为 entity 曾经做了过滤和适配解决,所以在 ui 层咱们能够放心大胆的用,不须要再写一堆莫名其妙的判断逻辑。另外因为 entity 是由前端本人定义的模型,无论开发过程中服务端接口怎么变,受影响的都只有 entity 工厂函数,ui 层不会受到影响。

最初,在 ui 层咱们还剩下令人头痛的技术栈迁徙问题。整个团队目前应用 vue 的我的项目有 10 个,按迭代频率和我的项目规模迁徙的计划能够分为两类:

  • 迭代频繁的大利用:次要包含代码行数较多、逻辑较为简单的几个中大型利用。这些利用想要一把梭间接实现迁徙老本极高,但同时每个迭代又有相当的需要。基于这种状况,对于这三个利用咱们采取了微前端的形式进行迁徙。每个利用别离起一个对应的 React 利用,对于新页面以及局部逻辑曾经齐全和 ui 解藕迁徙老本不高的业务,都由 React 利用来承接,最初通过 module federation 的形式实现交融。
  • 迭代不频繁的小利用:剩下的利用均是复杂度不高的小利用,这部分利用迭代的需要不多,以保护为主。因而咱们的计划是对现有逻辑进行整洁架构重构,在 ui 和逻辑分层之后间接对 ui 层进行替换实现迁徙。

4. 后续

通过整洁架构咱们造成了对立的编码标准,在前端利用标准化的路线上迈下了松软的一步。能够预感的是整个标准化的过程会十分漫长,咱们会陆续往规范中减少新的标准使其更加欠缺,短期内在布局中的有:

  • 单测即文档:下面提到了 usecase 通过依赖倒置来配合单测落地,后续团队冀望将一些业务逻辑的实现细则通过单测的形容来进行积淀,解决业务文档实时性的问题。
  • 欠缺监控体系:前端常遇到的 3 种异样包含 代码逻辑异样、性能瓶颈(渲染卡顿、内存不足等)、数据导致异样。对于数据异样,咱们能够在 entity 层映射的过程中退出对异样数据的埋点上报来填补目前监控的空白。(代码逻辑异样通过 sentry 曾经监控,性能监控对于中后盾利用不须要)

后续在规范逐步稳固之后,咱们也冀望基于稳固的标准进行一些工程化的实际(比方依据 mooncake 文档主动生成 adapter 层、基于 usecase 实现性能开关等),敬请期待。

参考链接:
The Clean Architecture:https://blog.cleancoder.com/u…
Module Federation:https://webpack.js.org/concep…
Anti-corruption Layer pattern:https://docs.microsoft.com/en…

* 文 / 陈子煜
@得物技术公众号

正文完
 0