Paradigm X

Vision quests of a soulhacker

什么是好的代码

本文将讨论一个简单又困难的命题:好代码的标准。注意,现在是 AI Coding 的时代,所以我们的命题自然要考虑 AI 写出的代码以及 AI 写代码的过程。

我们将使用一种模仿数学公理体系的表述方式来进行分析,以便于我们自始至终明确区分“确定的结论”和“有待验证的判断”。当然,目前我们还做不到真正数学级的严谨,但这件事实在太过重要,所以我们可以先接受这种“近似严谨”,再不断迭代优化它。

为了让分析的过程更容易理解,我们会先给出“前 AI Coding 时代”的共识(定义和公理),然后分析在当下有哪些公理依然成立,而哪些已经动摇甚至被颠覆。

定义与问题域

计算机软件

计算机软件 Computer Software代码数据 及相关 文档 的集合,是与硬件相对的非有形部分,它们共同协作让计算机执行特定任务,满足用户的特定需求,是用户与硬件之间的桥梁。计算机软件的主要组成部分:

  • 代码 Code :为完成特定功能而设计的指令序列,告诉硬件如何工作,也称为 程序 Programs ,是本文讨论的核心对象。
  • 数据 Data :支持程序运行所需的信息。
  • 文档 Documentation :关于软件的开发、使用和维护说明,帮助用户理解和使用软件。

软件系统与软件模块

当我们说 计算机软件 ,通常是从外部视角,将软件作为一个整体看待;而当我们试图从内部视角看一个软件,我们会将其视为一个 系统 System ,即所谓 软件系统 Software System 。一个软件系统可由若干 子系统 Subsystem 组成,而子系统又可由更小的子系统组成,子系统也可称为软件 模块 Module

软件规模与复杂度

软件系统的 规模 Scale复杂度 Complexity 是相关但不同的概念。

  • 软件规模 通常指软件系统的代码行数、功能点数、用户数量、数据量等客观的规模性指标。
  • 软件复杂度 通常体现在维护和修改软件的代价或困难程度上。

一般来说,软件规模越大,内部联系越复杂,复杂度通常也越高,体现在对软件的修改和验证代价上。但一个规模很大的软件系统也可以有较低的复杂度,如果它经过特定设计,使所有可能的修改都限定在一定范围内,且高度可验证的话。

软件修改与验证

无论增加、移除还是变更软件的功能,都只能通过对软件系统的修改实现。对此修改通常需要进行两方面的验证:

  • 修改确实实现了既定目标,即按照需求规格增加了新功能或移除、变更了旧功能。
  • 修改没有影响需求规格以外的其它既有功能,这通常称为 回归验证 Regression Testing

软件生命周期与版本迭代

软件 生命周期 Life-cycle 指软件从概念形成到最终退役的完整过程,通常包括 软件定义软件开发软件维护 等阶段。

绝大部分软件还会有 版本迭代 Product Iteration 的过程,新版本会继承旧版本生命周期(定义、开发、维护阶段)的所有成果(代码、数据和文档),在此基础上再经过定义、开发和维护阶段来完成自身的生命周期,进而被更新版本的生命周期所继承。

软件生命周期与迭代带来软件规模和复杂度的持续增加,直到旧软件被彻底废弃、完全重新构建新的替代软件为止。

旧公理体系

接下来我们列出在“前 AI Coding 时代”被广泛实践验证的一些公理,包括关于软件开发根本性困难的四条公理(公理一至四)和关于控制软件规模与复杂度的三条公理(公理五至七)。

公理一:需求变化公理

软件需求必然变化。软件需求在软件全生命周期中都可能变化,其不同阶段的变化可能性可以调控但无法消除。

公理二:需求模糊公理

未完成的软件的需求无法被完全描述。所有涉众在软件完全呈现并可使用之前无法完全统一对功能及非功能性需求的认知与表述。这里的“涉众”包括功能的各价值相关方和开发团队中的产品定义、开发等执行角色。

公理三:复杂度公理

一次对软件系统的修改,其失败概率与对其进行验证的难度成正比。在工程实践中,“失败概率”可以看作与“复杂度”、“难度”、“代价”近似的概念。

推论 :结合“软件修改与验证”定义可以推出,修改的失败概率受两方面影响:

  • 修改本身的复杂度,越复杂所需的验证就越难;
  • 修改影响的范围,影响范围越大,所需进行的回归验证就越多、代价就越高。

公理四:成本公理

开发者的能力与潜在产出可能无法被准确量化。

推论 :软件开发的成本和交付时间无法准确预估。

软件工程的根本性困难

以上的定义及公理一至四指出了软件工程的根本性困难:需求的变化与模糊性决定了软件系统的修改是常态;软件系统的复杂度增长使每次修改的难度和代价不断增加。

在不断变化的需求和不断增长的系统规模前提下,如何控制系统的复杂度和每次修改的复杂度是软件工程中架构层面的核心挑战。

软件工程中另外的两个核心挑战分别是:

  • 需求管理与控制:两个需求公理指出,杜绝需求的变化与模糊是不可能的,但可以通过一定的控制措施使其更可控。
  • 资源调度、计划与风险控制:成本公理指出这同样不可能完全精确,但同样可以通过一定的控制措施将风险与误差控制在一定范围内。

后面这两个挑战与本文主旨没有直接关联,所以不会深入讨论,只在需要时提及。

在“前 AI Coding 时代”,对系统复杂度及修改复杂度的控制,主要依靠的是软件代码的架构设计,即良好的“模块化”:如果我们能把复杂的软件系统设计为一个个独立的模块,每个模块可以独立地定义、开发与验证,模块与模块之间仅通过定义良好的接口进行交互,那么即使系统总的规模非常巨大,每次修改的影响也能限制在少数几个模块内,其复杂度和正确性就可控。

一直以来,构建大规模无错软件系统的希望,就在于通过模块化实现增量式构建:先积累小的、充分验证过的无错模块,通过组合这些模块来形成更大的模块,以及更大的模块……已经验证过的模块在系统扩大的过程中无需再次验证,这样就能维持以可控的代价建立越来越大的无错系统。

而实现这一点的前提是保证“模块化”的核心要求:高内聚,低耦合,以及良好的交互接口。也就是下面三条公理。

公理五:内聚公理

一个软件模块应专注于一个单一的、明确的功能或职责。如果不是这样,可以拆分成更多模块。

公理六:耦合公理

模块之间相互依赖的程度应尽可能低,理想状况下一个模块不应依赖于任何其他模块的内部实现,而只通过接口互相协作。

公理七:接口公理

软件模块应提供定义良好的、供其他程序模块使用的接口;定义良好的接口应尽量满足:

  • 责任分离 Separation of Concerns :接口只暴露必要功能,而隐藏其内部实现细节,调用方只与其交互。
  • 稳定 Stability :一旦投入使用,接口不应轻易修改,如需要新增功能,应定义新接口,而不是改动旧的。
  • 不可变 Immutability :不应修改接口传入的数据或状态,而所有 副作用 Side Effect 应局限在模块内。
  • 契约化 Contract Based :定义清晰的数据类型、函数、参数、返回值、异常等,形成一个完备的契约,如可能应与实现无关。

控制复杂度

公理五至七合起来,定义了一个强有力的约束,使满足约束的软件模块成为真正高质量的软件“积木”,可以放心地用于组合、构建复杂的软件系统;也使软件构建过程中每一步的复杂度都可控,因为我们总是可以通过分而治之的策略,将每一次修改限制在足够少、足够小的模块内,对其他模块的影响可以忽略,验证起来代价也就足够可控。

前 AI Coding 时代的标准

在“前 AI Coding 时代”,对“好代码”的评价标准,很大程度上源于上面的定义和公理体系,大体可以分为几个方面:

  • 基本
    • 正确
    • 安全与容错
    • 性能与效率
  • 复杂度控制
    • 模块化
    • 易于测试与验证
    • 易于扩展与变更
  • 团队协作
    • 清晰易读
    • 一致风格与约定
    • 文档化

我们通常会使用一系列测试方法来确保软件的正确、安全和性能;通过代码评审和其他相关软件工程实践来确保代码架构符合模块化要求,使得对软件修改和验证的复杂度不超出可控范围;通过软件工程的过程规范来降低在长期多人协作过程中形成的人为损耗。如果这三方面都能保持得很好,那么软件项目就不会失控。

那么,在“AI Coding 时代”,这些结论还成立吗?

AI Coding 时代的不变量

在这个新的时代,由 大语言模型 Model 、模型可调用的 工具 Tools 和人类的 指令 Instruction 共同组成了 智能体系统 Agentic System ,帮助我们完成各种软件开发工作。下面我们会用 智能体 Agent 来代表这样的人工智能体系统。

可能你已经发现了,在 Agent 给软件工程领域带来的巨大改变背后,保持着一些关键的不变量,例如:

  • 由于定义和使用软件的基本逻辑并未发生变化,所以关于软件的基本定义没有变化,人类仍然是软件定义的最终决策人和责任人。
  • 由于目前 Agent 尚无法生成可自证的软件系统,所以关于软件验证的定义没有变化,人类仍然是验证软件修改和验收的责任人。

相比这些确定的不变量,还有一些比较复杂的维度,我们下面重点讨论其中两个。

代码模块化与可维护性

一个关键且有些复杂的问题是:控制软件系统和每次修改迭代的复杂度还有那么重要吗?这个问题的答案直接影响代码的模块化要求是不是不变量的认定。由于大语言模型的能力还在快速演进中,对超长上下文的处理能力完全有可能在可预见的未来得到显著加强,强到可以一次读取和理解整个 codebase (对大部分规模的软件项目),所以我们对这个问题尚无法完全下定论,但基于下面几点,我们认为控制复杂度仍然是代码质量中的关键分量:

  • 大语言模型能处理超大量的上下文,并不代表能非常高效地处理,可能带来飙升的成本和下降的质量,在小而精的上下文中工作,在很长时间里仍将是最高效的方式(从时间和金钱两个层面)。
  • 在很长一段时间里,复杂的软件系统和现有的软件系统仍然需要人类与 Agent 共同维护,良好的模块化设计是人类(参与)维护软件系统的前提条件;在复杂系统上排错也是目前离不开人类的一个场景。
  • 模块化代码更容易测试,我们可以更快更好地测试独立的小模块,而非整个巨大的系统; Agent 可以生成代码,但验证和测试仍需测试框架,如果代码不模块化,故障隔离会变得困难,导致后续的修复和修改更容易出错。
  • 模块化代码更容易复用,而久经考验的可复用代码可以大幅提升 Agent 的生成效率。
  • 由于 Agent 生成代码的效率远高于人类,即使维持模块化设计及拆分更多任务 iteration 需要花费一些时间,但只要能降低失败率,少返工,产出效率仍可比上一个时代提升一个数量级,足够好了。

目前大部分 Agent 并不会非常主动的进行模块化设计,所以定期进行模块化重构是当下最有价值的最佳实践之一。这类重构本身难度不大,成功率较高,而收益很大,可以控制系统总体复杂度,对齐与 Agent 关于代码架构的认知,形成长期有效的文档等。

代码可读性与文档化

和很多人的直觉可能不一样,代码的可读性以及文档化对 Agent 来说仍然很重要,某种意义上可能比对人类还重要。具体体现在:

  • 大语言模型本质上是语言理解与生成的工具,在 Coding Agent 的工作场景中,正确理解复杂的上下文是成功的基础,而代码本身携带的有价值信息(良好的命名规则、模块划分、函数定义、注释等等)越多,越能帮助模型生成正确的代码;而误导性的成分越多,越容易造成模型的误解与错判。
  • 以人为主的工程通常追求团队的稳定,但我们经常更换 Agent 工具, Agent 也不具备人类那样的长期记忆与稳定风格,所以更需要通过标准化代码风格和专门的约束文档来弥补。
  • 如果是人类与 Agent 共同维护的代码,我们可以通过一些特殊约定的标签来限制和规范 Agent 行为,比如通过特殊的注释标注保护不可更改的代码片段。

与建立专用文档与 Agent 互动相比,在代码中适用良好的风格,配合精心设计的协作标签,能更经济的使用上下文(因为 Agent 通常只访问与任务有关的片段),也符合人类维护代码的习惯。

AI Coding 时代的增量

大量由 Agent 生成的代码带来新的问题,也带来对代码新的要求,这里面最突出的是软件(自动化)验证的一些需求:

  • 开发环境安全: Agent 在开发过程中可能访问本地资源,可能访问(不安全)的网页,可能运行各种本地命令,这里面有巨大的安全隐患,非常需要一种安全强化但依然足以完成开发任务的环境;现有的虚拟化和沙箱技术足以支撑这样的环境,但需要良好的整合和产品化。
  • 代码运行安全:目前绝大部分情况下 Agent 不能确保生成代码的安全性,由于信息安全领域的特殊性,似乎也很难通过学习代码素材来解决这个问题,可能的一个路线是开发针对 Agent 生成代码的安全与可靠性扫描与检测工具,并将其融入 Agent 的标准工作流程中。
  • 更高效的软件验证流程:无论怎么控制复杂度,只能提升总体效率,不能确保最终交付系统的正确性,完备、高效、分层渐进的软件测试和验证仍是最后的关键节点,如前所述,如果我们给 Agent 生成代码的模块化与文档化提出一些强制要求,也许可以对此过程的自动化带来一些更好的机会。

这些问题其实一直都有,但由于人类生产代码的速度远不及现在(和可预见未来)的 Coding Agent ,所以在这个时代,这些需求变得更加重要甚至紧迫。

在大量自动化验证工具的帮助下,也许就能实现更好的人类与 Agent 间的分工,并推进代码双重 Owner 等全新的分工、责任与协同体系,把我们更快引向 软件工程 2.0

结论

通过以上的分析,我们可以得到如下判断:

  • 软件开发的基本范式还没有大的变化,软件工程中的核心问题(渐进式定义、复杂度控制、测试与验证)依然是我们面对的主要挑战。
  • 本文前半部分的定义和定理体系仍然成立,只在一些具体细节上需要进行新的解读。
  • 进入 Coding Agent 时代后,通过良好的代码架构来控制复杂度仍然非常重要,仍是最具效率的最佳实践;代码可读性和文档化的要求甚至高于过去;而对软件系统的测试与验证是亟待创新与突破的领域。

如此看来,在这个新的时代,好代码的标准并没有太大的变化;未来可能最大的变化是通过更可靠、高效的测试验证系统取代细致的代码评审,从而可以在软件系统总体复杂度受控的前提下忽略对代码细节的追求。