Cyrus Blog

FLAG{S0_H4PPY_C_U_H3R3} (>.<)

缺少粘合剂的糟糕架构:过度抽象、包装和设计

本文共 3.2k 字,预计阅读时间 10 分钟。

最近在公司组内换了一个项目,因为之前的项目存在一些问题(这一部分我可能会在另一篇文章中提到)。新项目主导的同学是有近十年经验,不过之前一直在同一家偏硬件公司就职;手头的这部分是波兰的同学和美国的华人同学写的,应该也都是 PHD 之类高学历的同学——我可能需要庆幸这一点可能让项目沟通和推进较为高效。

毕业后我回到公司大致结束半年 WFH 的生活(可惜工作餐相比年初难吃了一些),一周时间内我大概了解了这份 Java 代码的架构和设计,不得不说:过度设计、抽象和包装简直是 IDE 各种找不到引用的噩梦,对人而言更是灾难。

一般架构演进

对于大多数项目,最初的架构都是单体式的——hello world 的打印显然一般不需要分布式。随着代码的开发,或是依靠架构师的经验,我们会将代码分离出一些部分:可能是缓存、IO、配置。进而我们会自然的分离动态和静态内容,之后我们可能让业务集群化、适应和接入公司基础架构,等等。

这里我们并不希望过多讨论项目的早期架构设计,虽然早期架构设计对于一个项目的最小功能验证的完成、可扩展性等至关重要,但这和架构师的经验息息相关,我们很难得到一套较为理论化的方案。这里我们主要谈的是:当一个较为复杂的架构,面临更多的同学加入开发和维护,后续应该如何保证工作的高效?

当我们有了一系列的代码,他们通常会有一些通用的部分。自然而然,我们希望将他们取出来,直接作为基础库。另一方面,有一些相关的、但是不完全通用的部分,我们希望可以将他们抽象,经过一定的配置,可以实现较为普遍的功能——实际上大部分我们使用的基础库都是这样而来的,只有极少数、具有极高专业性的库可能不具有复杂抽象,他们可能通过二进制分发等黑盒方式被使用。最后,私有部分的代码通常不能被压缩,我们需要继续保留他们。一个理想的抽象如下图所示:通过构建一个可配置的中台,随着项目越来越多,只需要较少的浅灰色的「配置文件」即可完成深灰色「接近部分」的功能,我们有了更多的精力和时间去维护私有部分代码。

理想的抽象.png

过度抽象

很显然,理想归理想,我们很快就会走进过度抽象的灾难中。

在大多数场景里,不同项目的「接近部分」大小可能很不一样。这里可能会有人问:差这么多也叫“接近”?当然我们这里的接近是按照功能定义的:我们可以说 QQ 和 Wechat 作为即时通信软件很接近,也可以说王者荣耀和 Dota 作为 MOBA 类游戏很接近。但很显然,前者接近程度远高于后者。一个公司开发很多个功能类似却不同的即时通信软件,适应不同的平台——很多企业定制化 IM 就是这样被开发的,其效率远高于做几款玩法类似的却不同的游戏。

在现实场景中,可能接近部分都是一类算法、一类功能,其所需的输入、输出和参数差别可能很大;实际开发过程中会导致中台很快膨胀,变得无比巨大。在这样的中台下,即便每一个部分只需要用到一小部分功能,但为了和这个臃肿的中台一起工作,其配置文件也会十分繁琐。示意图大概是这样。

过度抽象.png
这样的中台典型开发人员是参与 A 项目的同学。公共部分非常简单的成为基础库,在实际使用中 X 同学构建了一套自用的配置文件,方便对 A 项目中经常修改的「接近部分」进行配置。随后 X 接触了项目 B,他沿用了同样的代码,增加了一些可配置化的内容,中台瞬间大了起来。之后 X 进入了项目 C;很可惜项目 C 的「接近部分」和之前很小,X 可能出于一些原因,继续使用了这个中台——不论是在公司内推广达成 KPI,还是习惯了自己的这套中台架构。但是由于实际代码差异巨大,需要很多的配置文件来支撑,甚至让中台变得更大。过度抽象就这样来了,带来如下损失:

  • 从 B 项目后期开始,中台逐渐变大,X 同学需要花费大量精力去维护;
  • 在 C 项目中,配置文件 + 私有部分的代码量远远超过抛弃中台所要编写的代码量,C 项目进度缓慢;
  • 因为进度缓慢,Y 同学被派来和 X 同学一起开发 C 项目,但是这个中台实在太复杂了,Y 花了很久很久才大致理解其内容,但是依旧不熟悉。Y 并没有对项目起到很大的推进作用;
  • 随后 Z、α、β 都参与了 A/B/C 等项目,但因为这个中台的原因,大家都花了很久才熟悉,常常因为自己没有做什么事情而苦恼。其中有一位同学写下了这篇文章;
  • 当然,X 的中台可能已经很难推广了,太难用了;
  • X 满心想着:我这个项目全靠配置文件完成了一小半功能,以后有 D/E/F 项目用这个中台肯定很棒,我离升职不远了!
  • HR 满心想着:X 的自己的项目建设缓慢,那个中台在公司内还啥贡献,被优化离他不远了……

过度包装

除了过度设计,另一个可能存在的架构问题在于过度包装。我们都知道“没有银弹 (No silver bullet)”,面对一大堆复杂的系统时,却常常有人想把他造成“银河战舰”(源自于隔壁 infra 组的小伙伴对某个大一统工具的戏称)。这样的问题非常显而易见:这些系统,从一堆本来看一看代码、补一补文档还能学会的东西,变成了一个庞大的、没有任何外部参考的、难以理解的东西。

我自己想了一下,这和过度抽象非常类似,所以就不画图了。可能唯一的区别是:造银河战舰可能连抽象都没有。更为可怕的是,银河战舰的缔造者们往往沉浸在「统一的架构/接口/平台」「无比完美的工作流」「造出银河战舰我就是宇宙之王」之类的幻想之中。

本来是几个常见的工具,只是同属于一类,当我们将他们聚合的时候,往往需要一个更大的区域包裹住这一类工具,从而将它们集成在其中。原来的工具可以通过一系列胶水代码粘合起来,虽说缝合怪可耻,但是往往能简单方便的完成一定的功能,新的员工来参与项目时也仅仅了解工具内的功能即可:输入是什么,输出是什么,哪些选项和功能。但在构造更大工具的时候,不免陷入了分布式退化:

  • 某个特定工具的使用者需要阅读多个工具的文档才能跑起来这个工具;
  • 某个特定工具的开发者需要阅读更多工具的文档才能明白这个工具的运行方式;
  • 提出了「银河战舰级监控」「银河战舰级运维」「银河战舰级 AI」等需求,银河战舰越来越庞大了;
  • 可惜银河战舰级附属产品并没有别的用途,别人小而美的工具链往往可以完成大部分工作,剩下小部分工作可能定制开发的成本远低于学会驾驶银河战舰的成本;
  • 甚至银河战舰可能都开发不出来,比上一节中至少开发了中台的 X 更惨:X 左转出门就可以说 ta 是中台架构师,而银河战舰没造完也很难自称银河战舰的总工程师……

过度设计

《人月神话》中提到过第二系统效应:

就一个人所做过的设计而言,第二个系统是最危险的系统,一般来说,都倾向于过度设计。

当他做第三或之后的系统时,之前的经验会互相印证,以确认出这类系统的一般性特色,而系统彼此之间的不同处,也会帮助他辨别出属于特殊和非通用的部分。

除了做些功能上的修饰之外,第二系统效应还有另外一项特征,那就是倾向于将之前已熟悉的技术发挥到淋漓尽致,但却没有留意到,这项技术早就跟目前项目的基本系统假设有冲突而不再适用。

坦言我设计的第二个系统可能压根没有跑起来。除此之外,我想起来了项目主导同学似乎正在他就职的第二家公司,以及在 ByteDance 中选择了远古的 Java 作为开发语言、Flink 作为技术选型,而没有理会我们一些自研的流批式解决方案。我对他较为信任(较为当然是相对而言),目前的项目也和之前他的工作经历很接近;我们的技术选型自然不会轻易更改,但就目前的体验而言,我有些担心:项目的一些设计细节可能需要更明确的需求我才会接受了。

总之,过两天我首先会给项目补一些文档,如果能帮架构变好一些自然是更棒了。

碎碎念

这里是一些比较轻松的碎碎念。

写到这里发现本文和王垠的 DSL 的误区 有一点接近了,那我们就谈一谈 Parser 设计好了。我参与的上一个关于 system call 的项目中我也写了一个比较复杂的 YAML parser,用于自动生成内核中的 BPF C 代码。当时和一起做的同学笑言:就算我们写了详细文档,这要是谁接手了这个项目,学会写 YAML 文件就要一两天。转眼间,进入这个项目时,我已经花了三天时间看怎么写 YAML 了(大雾)。

这三天里我经历了:Java Maven 怎么还要手动写 XML;明明应该是 required 的 field 却找不到就会直接 return null 还不报错继续跑;Parser 为什么去掉了一个 filter 整个 statement 都消失了……第一天我觉得学 Java 的这一天应该是我进度最慢的一天,到了第三天我发现写 YAML 才是进度最慢甚至还要进度条倒回去修改 Parser 里的小bug 的。

结论

我首先想说我毕业论文已经写完了,这不是毕设后遗症,即便大多数 blog 的文章并不会有结论这一章(X

当我们遇到过度抽象、包装和设计时,我们可以做什么:

  • 做一些粘合剂,把一些已有的东西串起来。很多情况下大家会意识到银河战舰不可行,造成几艘小航母还是没问题的。
  • 尽可能评估阅读代码+编写配置所需时间,如果超过自己写的时间,尽快另起炉灶,之后做粘合剂把串起来,避免支离破碎。
  • 如果看到这篇文章的是某个项目的架构设计者,希望对你有一些帮助。
  • 当然如果看到这篇文章的是架构职位的面试者,请务必记住不供参考,本文内容或许会产生负面效应(逃