很多时候我们认为项目里代码质量和可维护性发生持续下降的主要根源在于时间紧迫、需求变动频繁。这个时候不免就会产生一个想法:

如果产品需求更加明确,并且给予 足够的 开发时间,那么开发团队就可以 长期的保证 代码质量和可维护性。

那么事实上是怎么样的呢?这里需要打一个

“归因谬误”

甩锅

在开发中我们不免的总会主观把锅甩出去,这个是人之常情,这个锅一般会被扣到产品头上。 但是总结经验来看,我们得看代码质量是否以 不可逆 的方式持续的质量下滑。

当代码质量以 不可逆 的方式持续下降时,开发团队才是需要负主要责任的

—— 并且即使产品提供了足够的开发时间和更加明确的需求,我们也还是不能逆转整个项目的全面劣化。

换而言之,从开发角度而言,大概率的原因是因为 没有采用更合理的开发模型 ,从而使得代码的复杂度随着时间推移快速膨胀,乃至整个团队无法维护,差不多和下图这样:

随着维护人员的增加以及时间的推移,代码复杂度会快速上升,并且通常而言会以指数方式攀升—— 更可怕的是,这一现象在初期并不明显。

作为一个项目的主导者、维护者、成员、或者参与的贡献者,我们肯定是期望拥有一个斜率较低的,平滑的代码复杂度上升曲线,那么本篇文章主要试图在一些更抽象的角度上探索代码质量的提升和优化。

代码质量是什么 以及 —— 怎么评估代码质量?

在开篇,我们不得不首先提出一个问题:

代码质量是什么?

这是一个 非常难以回答 的问题,即使大家阅读过一些知名的图书 1,也会对代码的质量有着极大的困惑,因为不同的场景下的代码其对应的质量是 完全不同的

早在上世纪 90 年代就已经有和我们朴素认知类似的结论了 —— 比如 Fred Brooks 《No Silver Bullet—Essence and Accidents of Software Engineering》2 提到的那样,软件工程本身不存在银弹,也就是我们常说的: 没有银弹,这就导致了我们通常会用相对 更具有主观色彩 的东西(朴素的认知)去表述代码的质量:

  • 可拓展性(Extensibility)
  • 可维护性(Maintainability)
  • 可读性( Reliability)
  • 可测试性(Testability)
  • 可移植性(Portability)
  • 可重用性(Reusability)

这些老中医式的判断决定了——这种判断是非常依赖 开发者的经验,且具有 主观偏好 的。

最经典的就是对于给定两段代码,不同的开发者、工程师大致可以判断出哪一个可拓展性 好。

但具体到 好多少,需要 定量的分析 时,往往 大相径庭 。此外特别是 可读性 这个指标,基本上和主观偏好有着极大的关联。

那么这个时候就需要找一些定量指标了。比如在学界也有不少奇怪的论文去讨论这个问题:比如试图从 编程语言 与 bug 数量、类型 的关系来分析代码质量3

在这些文章中,通常用 bug 数量、Lint 规则、循环引用检测来作为指标进行研究。诚然,这种定性指标是比较容易得出具体结论的。然而他也是有很大缺陷的,主要是这些指标太细节了,非常的容易失效,甚至在我们的业务里面有些极端情况下会带来相反的效果。

另外就是对于对处理具体问题的帮助也有限,也就只能用来统一代码风格,约束和规避错误写法、减少常见的反模式等等,属于 必要,但是不那么有用的东西。

然后我们会想到 —— 讨论这些事情的最终目的往往是为了能够提升代码质量,那么在提升代码质量上又有什么困境呢?

与实际不符的编程原则

现在倒是可以聊聊怎么提升代码质量了,虽然代码质量一说是主观的,但是对于同一个人的同一个时期,他对于同一份代码的评估标准基本是一致的。勉强还是能够判断出是否提升了代码质量的。

所以我们倒是在还算能评价代码质量是否有所提升了,那么说起提升代码质量,其中最著名的两个或许是设计模式 (Design Patterns)4 和 SOLID5,它们至少上世纪 80 年代已经投入使用,直到今日还被广大开发者津津乐道,耳熟能详。

当然咯!现如今,我们会发现一个更有意思的事情,很多开发者开始对这些 奉为金科玉律 的编程原则产生了一些不同的看法 。

比如上图那样——如果我们先把所有代码进行归类、整理,比如所有的代码归为一体,那么通过简单的定性分析(好坏)就可以把整个代码分为两块:

  • 一块是糟糕的代码
  • 其中较小的一部分是我们普遍认为的好的代码
  • 再从这两块中寻找一个共性那就是干净的代码6

我们会注意到这样的几个事实:

  • 好代码中有干净的代码和不干净的代码
  • 在所有代码里面,好代码的数量总是少于坏代码的,因为能写出好代码的人就很少
  • 有很多代码是低质量代码,但是他们仍然是干净的代码甚至还是能很好的工作

这是一个值得思考的问题 —— 我们可以有 干净的代码,但是他们不一定都是 好的代码(或者说高质量的)。我们将大量编程原则作为金科玉律的同时忽略了一个显著的问题:

问题

我们在高质量的代码中可以提炼总结,并归纳出高质量代码的共性 —— 他们是好代码的特征(譬如符合 SOLID 原则)。

然而 好代码特征 跟代码质量之间仅仅只是有一定的相关性 —— 这并不意味着他们之间是因果关系的:

高质量代码都有某些特征,不意味着有某些特征的代码就是高质量的代码。

迷茫?

在经过短暂的思考以后,我们发现了这样一个事实:

  1. 我们没有一个工具可以帮忙在宏观上更好的考虑问题
  2. 我们没有一个合适的评估标准可以在微观上定量的提供辅助
  3. 我们过去常见的提升手段似乎并不是完全适用的

那么下面我们就试图使用一些抽象的手段去寻找一种合适的、概念性的方案去思考这个问题,我们不难发现现有的评价体系的问题:

  • 太过主观 —— 他们依赖于经验
  • 模糊 —— 他们是 “武功” 心法,只可意会
  • 具体 —— 他们过于关注表象而忽略了外层空间

那么我们就得朝这个方向寻找一些已有的工具、或者思想去解决他们。

产品的诞生

在聊代码之前,我们就不得不提提产品,产品的诞生才是代码世界得以生长的源泉,而这一源泉也是代码腐坏的起点(非贬义)。

朴素的产品研发哲学

事实上,在现如今,绝大多数的产品仍然使用着 “古老而有效”(中性概念) 的原始产品研发方案他们的流程大致都差不多:

通俗的可以解释为 :

从产品需求文档到落地。

这个里面经历的步骤大概都差不多和下面这个图差不多(高度简化):

产品会作为一个传话人和设计师对接完成以后,最后把需求给到工程师,那么作为全流程链的末尾——工程师最终实现代码。

这种方式在产品那边有个专有名词 —— Request-driven design ,也就是常说的 需求驱动设计 。

根据上级、运营、市场、客服、别的产品等需求方所提的具体需求,直接进行产品架构、功能设计。

这种设计方式,用咱们的黑话来说,是一种面向过程的设计方式,同样的它也有一个专有名词—— 设计模式/贫血模型

我们从 DDD7 那边直接把 贫血模型有以下几大致命缺点拿出来就可以说道说道了:

  1. 创建的对象不准确,直接影响产品和开发对业务的正确把握和扩展
  2. 业务逻辑分散,业务难以复用
  3. 业务间耦合度高,迭代及维护成本极高
  4. 名词定义不一致,开发与业务出现沟通问题

通常如果需求的 生命周期很短 ,那么对于大家而言这就是个 一次性的需求,也不需要花费心思。故而在这种模式下很少有产品团队能够 始终维护完整的产品文档,让他们跟线上的功能 一一对应。假设事实果真如此(是个一次性需求)——我们也确实不需要非常关心这些问题,可惜令人苦恼的另外一点就是,现实却恰恰相反。

一旦产品经理换人之后(更极端的情况是,经过很多轮次的产品迭代以后),由于产品文档在迭代的过程中,语义不断丢失,又或是因为产品文档的缺失。

工程师们就需要通过读代码来反推产品逻辑 —— 这是我在职业生涯里面无数次的经历。

这个行为可是会出大问题的,因为代码是一种专有词汇,他们是在技术用语的上下文里编写的。所以他们其实很难反映产品里的概念,更加不用说业务概念了。这不但要消耗工程师大量的精力去寻找“原初开发者”的思路 ,更有甚者还需要添砖加瓦,从里面揣摩出原意 —— 和抽奖似的。

那么我们也不难看出这种情况下,为什么这种方式会快速的使得整个代码质量随着人员的增加和时间的递进而快速增加了。

我们把一个人产出的对于产品的逻辑复杂度作为纵轴,时间作为横轴,那么随着需求的递进(他会随着时间而膨胀),我们可以的到这一一个图:

他差不多是一个线性关系的。但是因为每一个人对于某一块功能的理解,是很难做到完全一致的理解,所以他在相互作用时会产生影响,譬如下面又有了两个人:

这种情况下, A 和 B 他们的概念如果有所不一致,那么他们 不一致的地方 最终也会反应在整体复杂度中,当然他们 一致的部分不会 增加整个系统的复杂度。

也就是说在这个场景下,两个人的逻辑概念的复杂度并不是取两者更高的一部分,而是取高值的同时,再将差异值的绝对值相加。

他大致会变成紫色线这样,但是这只叠加了2个人,如果有个更多人参与进来,有更多的需求、概念、逻辑步骤相互影响呢?

这不难看出逻辑的复杂度会随着人数和时间的增加快速的、指数级的增加,而且凑巧的是,项目越小越初期的时候,由于概念足够少、理解差异也小,所以这一膨胀现象非常不明显,但是越到后期他们互相理解的差异也越大。

所以还没到代码这一层呢,我们的项目就飞快的腐化了,可想而知到时候的代码会有多可怕,那么在逐渐的迭代中,最终代码就只会走向不可维护的状态,只能重构了。

悲剧

人与人的悲欢并不相通……你不能指望两个人能完全互相理解彼此——还是在逻辑概念如此复杂的情况下。 — icepro

思维方式的改变

对于更加复杂的研发场景其实早就有概念了,在前文提及的 DDD 就是比较出名的一种。

比如上文提及的 “腐化概念” 本质上是由于贫血模型带来的 失忆症

那么回到复杂的产品研发情况下,我们期望在这种情况下 最本质的 要解决的问题是什么?

在我看来是一种思维方式的改变,比如我们在编写代码中通常会在 “受限” 环境下思考问题,而在特别复杂的产品下,我们必须要脱离开 “受限” 这个概念,因为一旦有了限定,就决定了你直接从细节开始入手。

我们需要重塑我们的思维模式,忘掉那些琐碎的细节,从上层去思考事物本来的模样。这就带来了一个疑问,产品领域的上层概念是什么?或者说产品的本质概念是什么?

在我们处理一个产品的过程中,其实本质是一种知识的转换路径。

在接触的各个人将自己拥有的知识放置在一起,最终产生了代码。但是事实上在各个不同的人中间,知识是不能互通的。

我们必须在各个不同所属专业领域的人中建立一个通用的交换语言(独属于当前上下文的)来进行 “通讯”。

类似于上图那样(这也是经常在 DDD 介绍里面常见的概念图),至于最终是不是使用 DDD 作为标准来实现这不是一个必然的结论,因为我们已经可以从一些具体的事物上判断出目前项目的情况是糟糕的还是良好的了:

  • 业务里的实体、流程、关系是否清楚
  • 业务里的概念定义是否明确
  • 所有参与项目的人,是否能够无障碍的了解整个项目相关的概念,是否所有人都能正确理解上下文

那么在这种情况即使项目在不停的流转,概念不停得新增,那么在有这些抽象概念来作为媒介、文档、规范的情况下,我们很容易就能将不同人理解上的差异大幅减小,换而言之就减少了整体代码复杂度的膨胀。

那么一旦整体代码的复杂度膨胀速度有所降低,一定程度上可以减少代码质量降低的速度。

脚注

  1. 如《代码整洁之道》等,这些书籍往往具有较强的滞后性

  2. HARD, DOES IT HAVE TO BE. “No SILVER BULLET ESSENCE AND ACCIDENTS OF SOFTWARE ENGINEERING.” Information Technology and Society: A Reader (1995): 358.

  3. https://web.cs.ucdavis.edu/~filkov/papers/lang_github.pdf

  4. https://en.wikipedia.org/wiki/Software_design_pattern

  5. https://en.wikipedia.org/wiki/SOLID

  6. 指完全符合所有编程规则的代码

  7. https://en.wikipedia.org/wiki/Domain-driven_design

本文标题:浅谈代码质量

永久链接:https://iceprosurface.com/2022/code-quality/

作者授权:本文由 icepro 原创编译并授权刊载发布。

版权声明:本文使用「署名-非商业性使用-相同方式共享 4.0 国际」创作共享协议,转载或使用请遵守署名协议。

查看源码: