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

2021-04-02 08:31:03 浏览数 (1)

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

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

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

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

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

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

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

原始形态:单体架构

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

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

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

  • cloc
  • git
  • framework
  • architecture
  • ……

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

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

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

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

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

复用形态之模块化

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

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

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

代码语言:javascript复制
├── framework├── psa├── src│   ├── app│   ├── bin│   ├── domain│   ├── infrastructure│   └── lib.rs

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

复制 over 复用

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

插件化的架构演变

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

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

代码语言:javascript复制
├── 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 里,我们都在尝试对系统的分层进行一个评级。而这个评级的其中一个依据是通过依赖关系,来确认各个模块之间的引用关系,从而判断系统的分层架构是否是符合需求的。

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

0 人点赞