6

分层架构演化:从单体的插件化演化所引起的思考

 3 years ago
source link: https://www.phodal.com/blog/layer-architecture-evolution/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Posted by: Phodal Huang March 15, 2021, 8:28 p.m.

最近,在为 Coco 优化分层架构之时,我陷入了各种决策困难之中。所以我通过不断地延迟决策,以摸清更适合现有系统的现状。换个简单来说,在危险边缘徘徊,以期待能获取最大的收益。

在设计新的架构时,我们总会凭借原先的经验,并结合业务现状的需求,并根据未来的需求做出我们的设计。即:

  • 过去的经验。
  • 现在的需求。
  • 未来的方向。

种种因素的影响之下,它注定了我们无法设计一个满足所有历史时期的系统。未来会变成现在,现在会变成过去。

Coco 架构设计:从过去到过去的未来

原先对于 Coca 的各种设计问题,以及 Golang 对于多平台的支持问题等多方面的因素。迫使 Inherd 开源小组在 Coco 在初始阶段,便考虑了为 Coco 设计插件系统。直到最近,我们实现了插件系统之后,发现了原来设计的分层架构已经不满足现今的需求。

虽然,我已经知道新的分层架构应该如何设计,但是我并不想朝那个方向过去。我走走弯路,再看看是否存在一些更有意思的设计。

原始形态:单体架构

在设计初期,我在 Coco 中引入了类似于 Clean Architecture 的分层架构设计(不包含 Cargo 模块):

  • app,对应于用例(usecases)。
  • bin,对应于 controller。在 Rust 的构建系统中,bin 目录的会被构建出可执行文件
  • infrastructure,对应于 基础设施,如调用 Git 的接口、访问文件系统等。
  • domain,即业务实体、领域模式,包含了系统的业务设计。

在 domain 目录下,根据了我们的四大基本业务,进行了二次划分 :

  • framework
  • architecture

尽管,我一直在说我采用的是类似于 Clean Architecture 的分层架构。但是实际上,并没有采用其中一些重要的设计,比如说通过依赖反转来控制流向的问题。从个人的角度来看:

  1. 它带来一定的架构复杂度,需要不断地传递相关的架构知识,能否在开源项目中推广,有待商榷。
  2. 后续可以通过重构来转换。我并非非常资深的架构专家,所以以学习为出发点更方便。

作为一个单体应用,这个分层结构凑合着:

  1. 不算太复杂,还能让开发人员知道哪的代码往哪里放。
  2. 可以按需演化为 Clean Architecture。
  3. 模块可以进一步按业务拆分。

故事的开始还是蛮美好的。

复用形态之模块化

为了在多个不同的系统/应用之间(即 Coco 项目的代码提供给其它应用)复用代码 ,系统中产出一些独立的模块,如 psa、framework 等等。这也是一个非常常见的模块化的场景。模块化在不同的语言里都有一定的相似之处。

譬如:在使用方式上存在本地使用远程发布两种模式。在本地使用时,无需关注语义化版本等一系列的事项,只需关注于代码本身。一旦时机成熟,也就可以进化为可远程发布的模块。

从单体中出现模块化的一种典型形式便是,在代码库中以与源码同级的目录呈现。如下:

├── framework
├── psa
├── src
│   ├── app
│   ├── bin
│   ├── domain
│   ├── infrastructure
│   └── lib.rs

这里的 frameworkpsa 便是独立的模块,一旦其与其它模块的依赖关系解耦开来,那么它就可以作为独立的应用发布。

复制 over 复用

顺便提一句,对于模块化的代码复用来说,如果代码量较少,那么可以尝试复制一份代码,而不是复用做代码。这样一来,我们可以通过此来解耦依赖。

插件化的架构演变

同时,为了灵活地扩展系统的功能,我们设计了插件系统。(事实上,更多地从意图上,我们只是为了减少包体积大小,这样可以方便地从 GitHub 下载)

于是乎,我们创建了独立的 plugins 目录,并在其中创建了对应的模块,如下的 coco_xxxx 即是插件。同时,我们使用了 plugin_manager 来作为插件的管理器(事实上,后面证明了,这个 manager 不应该独立作为一个模块存在):

├── framework
├── plugin_manager
├── plugins
│   ├── coco_container
│   ├── coco_pipeline
│   ├── coco_struct
│   └── coco_swagger
├── psa
├── src
│   ├── app
│   ├── bin
│   ├── domain
│   ├── infrastructure
│   └── lib.rs

从设计和演进的角度来看,问题并不多,也可以使用。

演进:未来的未来

好了,由于经验上的不足,我们就面临了之前没考虑到的问题。

提取核心模型

从设计思路上来看,我们本应该在原先的架构模型中,提供一个 core 模块。而在这个 core 模块里呢,则用于提供一些核心的代码给插件应用

所以,很快地我们就创建了一个 core_model 出来了。我的本义也就只是提供一个核心模型。我不想像一些插件化项目中,在 core 中提供大量非核心的代码。

只是呢,随着第一个模型复用需求的出现,很快地就有了第二部分、第三部分。

再次抉择:基础设施层的改造

而插件之间除了模型的复用,还会有基础设施的复用。而这些代码,我又不想放到 core 里,所以就又需要抽取中一个 infra 的模块,用来共享基础设施的代码。那么问题来了,我们应该如何选择?

  1. 将原有的 infrastructure 提取到主目录下,作为单独的模块存在。
  2. 双层infrastructure,即只提取共用的代码,到主目录下,作为独立的模块。

从架构设计的思想来看,我是支持双层基础设施的存在。过多的无意识地复制这些公共代码,会导致这个包大小的进一步膨胀。一个典型的例子,就是我们在一个被称为 common 包的 jar 包里,看到一个 common 子包下,还有 common 目录的存在,即 xxx-common.common.common。

小结:架构的持续演化

故事就到这里了。哪怕一个再小的项目,它的架构模式也会随着系统的开发,不断地演化。如果不加以控制,那么系统可能会推动控制。而演进本身呢,也不会是一帆风顺的。

不过,我在思考一个新的东西,关于『分层架构适应度函数』。

Yiki:分层架构适应度函数

无论是在 Coco 还是在 Coca 里,我们都在尝试对系统的分层进行一个评级。而这个评级的其中一个依据是通过依赖关系,来确认各个模块之间的引用关系,从而判断系统的分层架构是否是符合需求的。

通过解析模块之间的引用关系,可以帮效地帮助我们厘清系统模块之间的合理度。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK