{T}

软件工程实践 | 宏观视角·设计文档·代码阅读·发布与版本管理·质量管理·开源与外包·迭代规划·未来展望

章节导言

软件工程只有 50 年的历史。相比动辄跨世纪的自然科学,它是一门极其年轻的学科。正因为实践时间太短,我们对其认知仍然肤浅。但恰恰是这门年轻的学科,却承担着人类有史以来最复杂的创造性工程活动。

软件工程的核心矛盾可以用一句话概括:软件工程本身是不确定的、快速变化的,但软件项目的管理却期望达到确定性。 我们的目标,是在大量的不确定性中找到确定性——这才是软件工程最核心的点。

架构师不是一个纯技术岗位。从软件工程的视角来看,架构师要对软件工程的执行结果负责——包括按时按质迭代发布、敏捷响应需求变更、防范质量风险、降低迭代维护成本。这意味着架构师必须关注团队协同、质量管理、外包决策、版本规划等"非纯技术"领域。

核心问题

  1. 软件工程与传统工程的根本差异是什么?为什么传统管理方法失效?
  2. 如何将团队共识精确表达为设计文档?设计文档的核心结构是什么?
  3. 阅读别人的代码有哪些方法论?如何从代码中反向还原架构设计?
  4. 发布单元与版本管理的核心哲学是什么?容器化为何是终极方案?
  5. 软件质量管理为何"反直觉"?单元测试与高频交付的核心价值何在?
  6. 什么该自研、什么该外包?开源、云服务、外包的决策框架是什么?
  7. 版本迭代规划在不同阶段的战略焦点有何不同?
  8. 软件工程的未来走向何方?AI 会替代架构师吗?
图表渲染中…

一、软件工程的宏观视角

1.1 软件工程的两大本质特征

软件工程与传统工程截然不同,其根本差异体现在两个维度:

其一,不确定性。 大型软件系统中几千甚至几万人参与,却没有两个人的工作是重复的。每个人每天都在写新的代码、做新的工作。软件工程从事的是创造性活动,而创造意味着大量试错——因为我们没有做过。涉及的人力、时间、业务变数,都远超传统工程。

其二,快速变化。 建筑工程完工即结束,极少变更。但软件生产出来只是开始——只要软件还在服务客户,迭代更新就永不停歇,从诞生到消亡,始终在变化。这与传统工程"一锤定音"的模式大相径庭。

这两点导致一个根本性的管理悖论:

图表渲染中…

1.2 架构师的职责:对工程执行结果负责

架构师的职责远不止"边界划分和模块规格定义"。从根本目标来说,架构师要对软件工程的执行结果负责,具体包括:

职责维度具体内容
交付质量按时按质进行软件的迭代和发布
响应能力敏捷地响应需求变更
风险防范防范软件质量风险,避免质量事故
成本控制降低迭代维护成本

这意味着架构师至少有两项关键能力超越了纯技术范畴:

  • 用户需求的解读能力:尤其是需求演进的预判能力。关键不在技术,而在心态——心里得装着用户。
  • 产品设计的参与能力:产品边界的确立不是纯需求问题,也不是纯技术问题,而是两者合而为一的过程。

深度注记:架构师深度参与产品边界决策是降低后期架构腐化风险的关键。某功能是否做成插件化、API 是否版本化、数据模型是否预留扩展字段——这些既是产品决策,也是架构决策。

1.3 软件工程生命周期

图表渲染中…

关键认知:软件工程不是一个项目,而是无数个项目。 每个项目只是软件工程的一个里程碑(Milestone)。其生命周期以数年甚至数十年计。

1.4 开发方法论演进:瀑布→敏捷→DevOps

维度瀑布模型敏捷迭代DevOps
变更成本越晚发现越昂贵每个迭代都可调整持续交付,实时调整
确定性早期追求完整规划在迭代中逐步明确自动化注入确定性
适合场景需求稳定、一次性交付需求不确定、持续演进互联网产品、云原生
风险暴露集中在后期分散在每个迭代全流程质量内建
与软件工程本质的匹配度较低较高最高

许式伟的核心判断:现代软件工程的实践,本质上是在瀑布模型的基础上增加了迭代闭环。瀑布模型描述了单次迭代的过程,而软件工程的真实形态是无数个瀑布的螺旋上升。

1.5 团队共识管理

在不确定性中寻找确定性,最核心的手段是团队共识管理。架构师的首要工作不是画架构图,而是建立团队共识——只有共识才是可靠的协作基础。

共识的精确性决定了协作的效率。模糊的共识(口头约定、会议纪要)会在执行中产生歧义,导致返工甚至冲突。

共识载体模糊度示例
口头约定极高"这个接口返回用户信息"
会议纪要"接口返回用户基本信息"
设计文档"接口返回 name, age, email"
接口契约GET /v1/user/{id} → {name: string, age: int, email: string}
可执行代码编译通过的类型定义

许式伟的核心主张:共识必须尽可能精确。最精确的共识是代码级别的接口定义和数据结构规格,而非架构图或会议纪要。

共识管理的反模式:

  1. 架构图即共识:架构图不精确,无法作为协作契约
  2. 文档写完即弃:过时文档比没有文档更危险——它误导而非帮助
  3. 口头约定替代文档:会议上的口头约定,到执行时各人理解不同
  4. 过度追求完美文档:文档的价值在于精确,而非篇幅

1.6 确定性注入的工程手段

所有工程实践——版本管理、自动化测试、持续集成、代码审查——本质上都是为系统注入确定性的手段。

实践手段注入的确定性付出的代价
版本管理代码可回溯、可对比仓库管理成本
自动化测试行为可验证、可回归测试代码维护成本
持续集成构建结果可预期CI 环境维护
代码审查质量门槛可执行人力时间投入
接口契约模块边界可依赖设计约束成本

Trade-off:确定性 vs 灵活性

  • 过度追求确定性(如过重的流程)会扼杀创造力与响应速度
  • 过度追求灵活性(如无约束的变更)会导致系统失控
  • 最佳实践:在关键节点(接口契约、发布基线)追求确定性,在实现细节上保留灵活性

二、设计文档

2.1 为什么写设计文档

设计文档是团队共识的核心载体。无论是产品文档还是架构文档,本质都是团队内及团队间协同的共识载体

许式伟的关键判断:设计是软件工程中的头等大事,我们应该在设计中"多浪费点时间",这样的"浪费"最终会得到十倍甚至百倍以上的回报。

2.2 设计文档的统一结构

所有设计文档的内容组织逻辑是相通的,无论产品设计还是架构设计,都遵循四段式结构:

要素写法要求核心关注点
现状不要长篇累牍。陈述与改变相关的重要事实,强调存在性和重要性我们在哪里
需求不需要长篇累牍。痛点够痛大家都知道,这是对痛点和改进方向的共识确认问题是什么
需求满足方式详写。包括交付物规格和使用界面(接口)做成什么样
实现原理数据结构 + 算法。用 UML 时序图或伪代码表达怎么做到

核心公式:程序 = 数据结构 + 算法。这是设计文档"实现原理"部分的指导思想——数据结构可以是内存数据结构、外存数据结构、数据库表结构;算法可以是 UML 时序图或伪代码。

2.3 产品设计 vs 架构设计的维度差异

产品经理与架构师是一体两面,对人的能力要求相似,但关注维度不同:

维度产品设计架构设计
关键词用户需求、技术赋能、商业成功用户需求、技术实现、业务迭代
交付物规格产品原型网络 API 协议 / 包导出的公开类或函数
实现原理UserStory 设计——业务流怎么完成UserStory 的程序逻辑实现
数据结构业务对象模型内存/外存数据结构、数据库表结构
算法业务流程UML 时序图、伪代码

2.4 接口先行,实现随后

在描述交付物规格时,有两个核心关注点:

  1. 接口是否足够简单,是否自然体现业务需求
  2. 尽可能避免接口变更,接口要向前兼容

对于系统的概要设计:

  • 第一关心的是模块关系
  • 第二关心的才是各个模块的核心接口(这些接口能把关键 UserStory 串起来)

模块间依赖关系的三种接口类型:

  • DOM API:常规功能调用
  • Event:事件发送与监听
  • Plugin:插件注册

深度注记:以 MVC 为例——View 监听 Model 层的数据变更事件(Event),View 转发用户交互事件给 Controller(Event),Controller 将用户交互事件转为 Model 层的 DOM API 调用。通过接口类型表达模块间依赖关系,比画流程图更有抽象力。

2.5 概要设计 vs 详细设计

维度系统概要设计模块详细设计
关注焦点不同模块的配合关系模块内部实现
数据结构不需要交代必须先交代清楚
UserStory讲清楚模块间协作流程讲清楚内部业务流程
伪代码必须——表达模块间交互语义必须——表达业务逻辑

2.6 多方案对比决策

当识别出多种实现方案时,决策流程如下:

  1. 概要描述各方案的本质差别
  2. 多维度对比(易实施性与可维护性、时间复杂度与空间复杂度)
  3. 业务倾向性判断:绝大部分业务工程效率优先(易实施+可维护为先);成本/性能敏感业务性能优先
  4. 确定主方案后,判断两方案比较优势是否显著
  5. 如果显著,以主方案写一篇文档并完整展开
  6. 如果不显著,分别写两篇独立文档——应被鼓励

Trade-off:架构图 vs 接口定义

  • 架构图有助于理解系统分解逻辑,但表达非常粗糙
  • 接口定义精确无歧义,但缺乏全局视角
  • 最佳实践:架构图辅助理解,接口定义作为正式契约。模块关系图 + 核心接口规格,两者缺一不可

Trade-off:单方案 vs 多方案文档

  • 单方案文档:聚焦,撰写成本低,但可能错过更优解
  • 多方案文档:论证充分,但投入大
  • 最佳实践:当两方案比较优势不显著时,写两篇独立文档。设计是头等大事,在此"浪费"时间回报百倍

2.7 华仔的架构设计文档模板

华仔提供了一个完整的架构设计文档模板体系,包含两个核心文档:

备选方案模板包含五大部分:

  1. 需求介绍:描述需求的背景、目标、范围
  2. 需求分析
    • 5W:Who(利益干系人)、When(使用时间)、What(产出物)、Where(应用场景)、Why(要解决的问题)
    • 1H:关键业务流程(非设计方案,而是关键用例流程)
    • 8C:8 个约束和限制——Performance、Cost、Time、Reliability、Security、Compliance、Technology、Compatibility
  3. 复杂度分析:分析需求的复杂度(高可用、高性能、可扩展等),每个约束都应有详细的逻辑推导,避免拍脑袋决策
  4. 备选方案:至少 3 个备选方案,每个方案描述关键实现而无须描述具体细节
  5. 备选方案评估:360 度环评,内容会根据评估会议的结果进行修改

架构设计模板包含五大部分:

  1. 总体方案:从整体上描述方案结构,核心内容是架构图及描述,包括模块或子系统的职责描述、核心流程
  2. 架构总览:给出架构图及关键设计点说明

  1. 核心流程:消息发送流程、消息读取流程等
  2. 详细设计
    • 高可用设计(消息发送可靠性、消息存储可靠性、消息读取可靠性)
    • 高性能设计
    • 可扩展设计(如果方案不涉及,简单写"无"表示设计者有考虑但不需要设计;完全不写则可能被认为是遗漏)
    • 安全设计
    • 其他设计
    • 部署方案
  3. 架构演进规划:分阶段实施——第一期做什么、第二期做什么

2.8 架构图最佳实践:4R 架构定义与常见架构图

华仔提出画架构图的核心指导思想——4R 架构定义

软件架构指软件系统的顶层(Rank)结构,它定义了系统由哪些角色(Role)组成,角色之间的关系(Relation)和运作规则(Rule)。

画架构图四步骤:

  1. 明确 Rank:不要事无巨细地展现大系统的方方面面,明确要阐述的系统所属的级别(L0~L4),只描述该级别的架构信息
  2. 画出 Role:从不同角度分解系统,角色对应架构图中的区块、图标和节点
  3. 画出 Relation:画出角色之间的关系,对应架构图中的连接线,不同连接线代表不同关系
  4. 画出 Rule:挑选核心场景,画出系统角色之间如何协作来完成某项业务功能,对应系统序列图

描述 Role 和 Relation 的架构图称为静态架构图,描述 Rule 的系统序列图称为动态架构图

4+1 视图的局限:4+1 视图(逻辑视图、处理视图、开发视图、物理视图、场景视图)本身全面规范,但实际应用少,原因有三:

  1. 架构复杂度增加——分布式系统下微服务太多,Development view 难以表示
  2. 绑定 UML 图——UML 画架构图不美观,表达能力弱
  3. 理解困难——逻辑视图、开发视图和处理视图容易混淆

常见架构图类型

架构图类型定义使用场景画图技巧
业务架构图系统提供给用户的业务功能(类似 4+1 场景视图)产品规划、汇报、培训颜色标识状态、业务分组、区块对齐
客户端/前端架构图客户端和前端的领域逻辑架构整体架构设计、培训颜色标识角色、连接线表示关系、分层分组
系统架构图后端的逻辑架构("后端架构"或"技术架构")整体架构设计、培训颜色标识角色、连接线表示关系、逻辑分组
应用架构图后端系统由哪些应用组成(可部署运行程序)项目开发测试、运维部署颜色标识角色、连接线表示关系、复杂系统分域画
部署架构图系统具体部署方式(机房/网络/硬件)总体架构设计、运维规划用图标代替区块,更美观易理解
系统序列图某业务场景下角色如何配合完成业务功能结合静态架构图使用使用 UML 序列图

深度注记:华仔强调——目前业界并没有就架构图标准达成共识。TOGAF 的架构基本上到 CTO 级别才能接触,C4 模型的表达能力又不够。华仔整理的架构图类型在华为、UC、阿里和蚂蚁等公司验证过。核心原则:一张图的颜色数量控制在 3 种以内。


三、代码阅读

3.1 阅读代码是架构的反向工程

阅读代码是架构设计的反向过程。架构设计是从需求出发,自上而下地分解与构建系统;而阅读代码是从实现出发,自下而上地还原架构思想。这个过程类似于反编译——不是指令级的反编译,而是需要根据代码反推更高维的设计意图。

编译过程是有损的:大部分软件实体的名字在编译过程中被去除。即使有符号文件,精确还原高级语言代码仍然非常难——它需要模型推理,需要识别出熟悉的"套路",然后按照套路进行还原。

有产出的学习过程,才是最好的学习方式。 阅读源代码的产出应该是构建这个程序的思路——也就是架构设计。

3.2 阅读代码的四种目标

明确阅读目标是第一步,因为它决定了你能为此付出多大成本:

目标类型投入深度关键产出
评估引入第三方模块低——理解概要设计即可是否采用的决策
修复局部 Bug中——理解相关业务流程Bug 修复方案
以开源模块为榜样学习中高——理解核心架构思想可借鉴的设计范式
接手并长期维护模块高——理解全部业务流程完整的架构文档

3.3 系统架构理解流程

图表渲染中…

工具辅助

  • Go 语言go doc 可自动生成包的文档
  • 多语言通用doxygen 支持几乎所有主流语言
  • example 和 unit test 属于研究对象的使用方(客户),能辅助理解各软件实体的语义

3.4 业务实现机制理解流程

概要设计理清后,根据目标决定后续投入:

  • 评估第三方模块:到此告一段落
  • 局部 Bug 修复:聚焦 Bug 相关业务流程
  • 接手长期维护:梳理关键业务流程

核心方法论回归最基础的公式:程序 = 数据结构 + 算法

  1. 先理数据结构:类的成员变量、数据库的表结构,通常都有快速提取的方式。理清楚数据结构,事情就解决了大半
  2. 再理算法:各个 UserStory 的业务流程,画出 UML 时序图

深度注记:MongoDB 等弱 schema 数据库,需要通过代码理解 schema;历史 schema 变更可能使最新代码无法反映完整的 schema 演进。这是数据结构理解中的特殊挑战。

3.5 阅读代码的核心原则

原则一:有文档一定先看文档。 如果已经有架构设计文档,坚持自己从零开始反向理解是愚蠢的。但文档和代码很容易脱节,需相互印证。发生冲突时,及时修改文档到与代码一致。

原则二:理解概要设计是首要目标。 概要设计的关注点是:各软件实体的业务范畴,以及它们之间的关系。只有在必要的情况下,才深入研究实现机制。

原则三:代码即文档,阅读代码不可替代。 哪怕团队文档多么完善,阅读代码都是不可或缺的基础能力——代码永远反映真实状态。

原则四:阅读代码时可以顺手改代码,但有原则

  1. 不做大改动:限定单个函数内改动不超过 10 行
  2. 语义完全一致:包括所有 corner case(错误码、条件语句边界等)
  3. 补全单元测试:不管多自信,有改动就必须补全相关单元测试

Trade-off:自己理解 vs 前任交流

  • 自己从代码推导:成本高,但理解更深入
  • 找前任开发者交流:效率高,但依赖对方可用性和表达能力
  • 最佳实践:先自行梳理到有一定理解,再找前任交流。提前准备迷惑问题列表,争取一小时左右的交流机会

反模式

  • 坚持从零开始反向推导(已有文档却不看)
  • 试图完整理解所有细节(没有明确目标的情况下全面理解是过度投入)
  • 阅读代码后不形成文档(下一次接手的人需要重新"反编译")

四、发布与版本管理

4.1 发布单元:软件工程的分治基础

从软件架构设计可知,软件是分模块开发的。不同模块可能由不同团队开发,甚至有些模块是外部第三方团队开发。这意味着,一个软件工程的生命周期中,包含着很多个彼此完全独立的子软件工程。

发布单元 = 拥有独立迭代周期的软件实体。

发布单元与模块的区别:

维度模块发布单元
直观感受代码中的逻辑划分有自己独立的源代码仓库(repo)
迭代节奏与系统整体同步有独立的迭代周期
交付物功能性代码可执行程序 / 动态库 / 静态库 / 源代码本身
管理边界系统内部可被外部依赖

发布单元的输出类型:

输出类型说明
可执行程序 / 字节码完整可运行的程序
动态库(so/dylib/dll/jar)运行时链接的库
静态库(.a 文件)编译过程的半成品,由链接器组装
源代码本身如 Go 语言的价值主张

4.2 只读设计的确定性

一个打好了版本号的发布单元是只读的,不能对其做任何改动。 这句话包含两层含义:

  1. 不能修改发布单元自身包含的各模块的代码
  2. 不能修改发布单元依赖的外部模块(发布单元)的版本——例如将 opencv 从 v1.0 升级到 v2.0,这也是一次变更,需要修改自己的版本号

只读设计带来巨大的收益,因为版本是一个"基线",我们对基线的预期是确定性的。

只读思想在软件工程中被广泛运用:

只读层次说明收益
版本只读发布单元基线不可改确定性预期
业务只读开闭原则——模块业务范畴只读,接口稳定架构治理
数据只读函数式编程——不可变数据提高确定性预期
RDD 只读Spark 弹性分布式数据集——变换产生新 RDD重试、延迟计算、缓存变得简单

4.3 语义化版本

语义化版本(Semantic Versioning)是行业标准:

code
MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
版本段含义变更规则兼容性影响
MAJOR主版本号不兼容的 API 变更破坏性变更
MINOR次版本号向后兼容的功能新增不影响现有行为
PATCH修订号向后兼容的问题修复纯修复,无新功能
PRERELEASE预发布标识alpha / beta / rc不保证稳定性
BUILD构建元数据构建编号 / commit hash不影响优先级

版本管理策略对比:

策略适用场景典型案例
语义化版本(SemVer)库与 SDK、开放平台 APIGo 1.x、npm 包
基于时间的版本(CalVer)SaaS 产品、操作系统发行版Ubuntu 22.04、PyTorch 2.0
滚动版本(Rolling Release)内部微服务、Web 应用Arch Linux、内部服务

4.4 分支策略

策略特点适用场景复杂度
GitFlowmaster/develop/feature/release/hotfix 多分支有明确发布周期的项目
GitHub Flowmain + feature branch,PR 合并持续部署的互联网项目
Trunk-Based所有人在主干开发,短生命周期分支高成熟度团队、高频发布

GitHub 提供了完善的源代码质量管理闭环:

  1. 开发独立性:每个人极低成本建立 feature branch,工作未完成时对其他人不可见
  2. 质量检查机制:提交 PR 时可配置自动化检查(unit test / coverage / lint / code review)
  3. 开放设计:质量检查过程需求易变,GitHub 做了开放设计——开闭原则的又一次体现
  4. 回滚机制:合并后发现 Bug 可 revert PR,主干得到去除该功能的新版本

4.5 容器化是版本管理的终极方案

图表渲染中…
管理层级管控范围确定性程度残留不确定性
源代码版本管理自身代码依赖版本、操作系统、编译器
+ 依赖版本管理+ 外部依赖版本较高操作系统内核、编译器版本
+ 容器化版本管理+ 运行环境 + OS最高几乎为零

4.6 灰度发布与回滚策略

灰度发布是降低变更风险的核心手段:

策略原理优点缺点
金丝雀发布先将新版本部署到一小部分实例风险可控,影响面小推进速度慢
蓝绿部署维护两套完整环境,切换流量回滚极快(切回旧环境)资源成本翻倍
滚动更新逐批替换旧实例无需额外资源回滚较慢,存在混合版本窗口
特性开关代码已部署,通过开关控制功能线上验证,无需重新部署代码分支复杂度增加

灰度发布典型流程:1% → 5% → 20% → 50% → 100%,每一步都观察关键指标(错误率、延迟、业务指标),指标异常则自动回滚。

回滚能力是底线:当线上出现问题时,优先回滚恢复服务,再择机修复。保证回滚能力是发布系统的底线要求。

4.7 版本兼容性管理

版本兼容的难度在细节——兼容主体功能容易,兼容错误码和低频分支行为才是魔鬼。

Trade-off:兼容 vs 不兼容(改名)

方案优点缺点适用场景
行为兼容升级无缝兼容细节多、风险高内部模块、可控依赖
改名不兼容干脆明确升级需重写、重测破坏性重构、内部模块

对于互联网服务(API),放弃兼容意味着放弃用户——不可接受。常见做法:为所有 API 引入版本号(/v2/foo/bar/v3/foo/bar)。

服务端 API 兼容性的特殊挑战:客户端可以强制升级,但服务端 API 的消费者往往不可控。因此服务端 API 的变更必须遵循更严格的兼容性纪律——接口只增不改不删

数据库 schema 变更是发布中最棘手的问题之一。标准做法:

  1. 双写阶段:新代码同时写入新旧 schema
  2. 迁移阶段:将历史数据迁移到新 schema
  3. 双读阶段:从新 schema 读取,但保留旧 schema 作为回退
  4. 清理阶段:确认稳定后删除旧 schema

每个阶段都是一次独立发布,每次只做一件事。

深度注记:Go 语言早期并没有官方的版本管理手段,导致社区出现很多版本管理方案。直到 go mod 机制终于统一了这一纷争。Go 1.5 引入 vendor 机制试图解决但未成功,Go 1.11 推翻重来引入 module 机制。这说明版本规划不排斥推翻重来,核心痛点必须持续攻坚。


五、软件质量管理

5.1 软件工程质量管理的反直觉之处

软件质量管理横跨整个软件工程完整的生命周期。但软件工程的质量管理思想与传统工程截然不同——甚至往往是反其道而行之

传统工程中,设计工作占比极少,重复性工作占绝大部分,检查清单(Check List)即可实现质量管理。但软件工程中,设计工作贯穿始终,连编码实现都依赖个体创造力。Check List 完全无法满足软件工程的质量管理需要。

5.2 开发期 vs 维护期:预见性是关键

开发期的时间跨度可能不长,但它基本决定了后期维护期的成本有多高。这意味着软件工程是需要有极强预见性的工程。开发期恰如其分地多投入一分精力,维护期就有十倍甚至百倍以上的回报。

设计工作的质量至关重要。但它在执行上不太有复制性,可复制的只是设计范式和设计思维。我们只能在这种执行的不确定性中找工程上的确定性。

5.3 质量属性

软件质量的维度远不止"有没有 Bug":

质量属性说明保障手段
可靠性(Reliability)系统在规定条件下完成规定功能的能力自动化测试、灰度发布
可维护性(Maintainability)修改代码的容易程度代码审查、设计范式
效率(Efficiency)资源使用的合理程度性能测试、profiling
可用性(Usability)用户使用系统的容易程度A/B 测试、用户反馈
安全性(Security)防范未授权访问和攻击的能力安全审计、渗透测试

5.4 质量管理的两大支柱

如何在不确定性中找到确定性?两大支柱:

  1. 自动化测试——通过可回归的验证注入确定性
  2. 持续构建与持续发布——通过高频交付降低变更风险

5.5 单元测试

单元测试是重中之重。 从测试成本两个维度看,单元测试都是最优选择:

维度单元测试集成测试
实施成本最低(Go 等语言内建支持)较高(需要额外基础设施)
修复成本最低(在问题现场发现)高数倍~数十倍(需定位+回忆思路+修复)

推广单元测试的认知障碍:很多公司推不起来单元测试,根因在于认知问题——我们不应把推广单元测试看作让大家多做一件额外的事情,而是规范大家做单元测试的方法。

实际上单元测试大家都会做——很少有人不经验证就直接交付。但验证方式五花八门:

常见"土"方法问题
print 调试不可回归,需人脑判断
可视化界面测试不可回归,依赖人工观察
单步跟踪不可回归,一次性消费

这些方法代价不低,且最大的问题是没有办法固化已知的 Bug,最大程度保留测试案例

核心认知:测试代码同样是开发成果,理应获得和功能代码同等重要的地位,理应被保留下来。

自动化测试的重要特征:

特征说明
自动化、可回归可反复执行,无需人工干预
静默(Quiet)没有错误时不说话,降低噪音
安全受控某案例失败不影响其他案例运行

5.6 测试替身(Test Doubles)

在单元测试中,被测模块的依赖项需要被替换为可控的替身:

替身类型定义特点适用场景
Stub返回预设值的简单实现最简单,只管返回被测模块只需依赖返回值
Fake有实际工作的简化实现有真实逻辑但走捷径需要更真实行为(如内存数据库)
Mock验证交互行为的对象验证方法是否被调用、调用次数、参数验证模块间协作关系
Spy记录调用的真实对象的包装基于真实对象,同时记录交互需要真实行为+交互验证

5.7 测试金字塔

测试金字塔是测试策略的经典模型:

  • 底层:单元测试(数量最多,速度最快,成本最低)
  • 中层:集成测试(数量适中,验证模块间协作)
  • 顶层:端到端测试(数量最少,速度最慢,成本最高)

核心原则:单元测试覆盖核心路径和边界条件,集成测试验证关键协作链路,端到端测试验证核心业务场景。

5.8 持续集成与持续发布

图表渲染中…

CI/CD 中的质量管理抓手:

抓手作用自动化程度
单元测试(unit test)验证模块行为全自动
覆盖率检查(code coverage)量化测试充分度全自动
静态代码检查(lint / vet)发现潜在问题全自动
代码互审(code review)人为质量门槛半自动
灰度发布(gray release)小范围验证半自动
A/B 测试数据驱动决策半自动

5.9 高频交付优于低频大版本

为什么高频交付更优?

  1. 回滚代价低:交付功能越少,因错误回滚的影响面越小。同时发布数十个功能,一个出问题影响整体——不如只回滚出问题的功能,放行其他所有功能
  2. 过程熟练度高:交付频率越高,对交付过程的训练越频繁,执行效率越高。交付成为自然习惯后,研发绩效与功能线上表现关联,面向客户价值
维度低频大版本高频小版本
回滚代价高——影响面大低——影响面小
问题定位难——变更太多易——变更少且近
过程训练弱——发布是特殊事件强——发布是日常习惯
研发心态做完功能就完事面向线上客户价值
系统化要求较低较高——需要完善的 CI/CD

核心洞察:发布频率越高,每次发布的风险越低。持续交付的本质不是"更频繁地发布",而是"让每次发布变得足够安全,从而敢于更频繁地发布"。

5.10 技术债务管理

技术债务是软件工程中不可避免的产物。关键不是避免技术债,而是有意识地管理技术债

债务类型产生原因偿还策略
故意债务为了快速验证市场假设而走捷径在验证成功后安排重构迭代
无意债务缺乏设计经验或对需求理解不足代码审查中发现,逐步改进
环境债务依赖版本过旧、工具链落后定期升级,纳入迭代规划

5.11 Go 语言的工程工具链

Go 语言从 1.0 开始就内建了完善的工程工具支持,体现了"工程能力内建"的理念:

  • go test:单元测试
  • go doc:文档生成
  • go vet:静态检查
  • go tool pprof:性能分析
  • go tool cover:覆盖率检查

这些工具链的完善,大幅降低了推广单元测试和代码质量检查的门槛。

Trade-off:单元测试覆盖率 vs 开发速度

  • 100% 覆盖率:质量最高,但编写和维护成本高
  • 0% 覆盖率:速度最快,但质量风险极高
  • 最佳实践:核心模块追求高覆盖率(80%+),周边模块适度覆盖。关键路径必须覆盖,边界条件必须覆盖

Trade-off:自动化 vs 人工审查

  • 全自动化:效率高、可回归,但无法判断设计合理性
  • 人工审查:能发现设计问题,但效率低、不可回归
  • 最佳实践:自动化处理可机械化检查的部分(测试、lint、覆盖率),人工审查聚焦设计合理性和业务正确性

反模式

  • 用 Check List 管理软件质量(软件工程没有大量重复性工作)
  • 单元测试是额外负担(正确认知:规范验证方式,而非增加工作量)
  • 发布是特殊事件(正确做法:让发布成为日常习惯)

六、开源、云服务与外包管理

6.1 跨组织分工的三种外部依赖形态

一个软件工程的生命周期中,包含着很多个彼此完全独立的子软件工程。这些子软件工程中,绝大多数并非自研——它们可能是开源软件、云服务或外部团队外包的模块。

依赖类型本质自主可控成本结构适用场景
开源软件社区驱动的公共基础设施高——源代码可见、可改零授权费 / 维护成本通用性强的底层能力
云服务供应商托管的服务低——黑盒调用、依赖 SLA按用量付费非核心业务的基础设施
外包定制化开发中——可约定交付物合同约定非核心业务的功能模块

6.2 开源软件:公司与公司的竞争是生态的竞争

开源软件是公司参与行业竞争的生态战略。公司与公司之间的竞争越来越像国与国之间的竞争,更像两大生态之间的竞争。大量的软件被开源出来,目的是:

  • 形成行业标准
  • 减少整个行业的无谓重复投入
  • 构建以自身为核心的生态圈

开源软件引入与治理流程

  1. 识别需求后,评估是否有成熟的开源方案
  2. 评估开源方案:License 合规性(GPL/Apache/MIT)、社区活跃度(commit频率/issue响应)、代码质量(测试/文档/架构)、维护团队稳定性
  3. 综合评估通过则引入,否则自研
  4. 引入后建立内部 Fork 管理(只做最小化修改)
  5. 持续跟进上游更新(定期同步)
  6. 深度依赖的开源软件应参与社区贡献(提升影响力)

6.3 云服务:基础能力的社会化

云服务本质上是"基础能力的社会化"。随着公有云越来越成熟,越来越多的基础能力被纳入云服务范畴:

  • 存储(对象存储 / 块存储 / 文件存储)
  • 数据库(关系型 / NoSQL / 时序 / 图)
  • 计算(虚拟机 / 容器 / Serverless)
  • AI 能力(NLP / CV / 大模型推理)
  • DevOps(CI/CD / 监控 / 日志)

云服务 vs 自建基础设施决策维度

决策维度自建倾向云服务倾向
业务核心度核心业务非核心业务
规模效应大规模小规模
合规要求强合规无特殊要求
技术壁垒低壁垒高壁垒

6.4 外包:软件工程分工的必然形态

不要排斥外包。 很多人对业务外包有天然的排斥心理,这是不理性不客观的。原因有二:

  1. 存在即合理:外包市场有几千亿美元的规模,不可能是一个不理性的事物
  2. 软件工程分工的必然性:没有人能做完所有事情,总是有一部分需要让别人去做

外包决策框架

  1. 识别功能模块 → 是否核心业务?核心业务必须自研
  2. 非核心业务 → 是否有成熟的开源/云服务?有则优先选用
  3. 没有成熟方案 → 定制化程度?低则外包给标准化服务商,高则看是否长期需要
  4. 短期需求外包给定制化团队(项目制),长期需求组建内部团队(或人月外包)

6.5 核心业务必须自研

所有围绕目标用户群体需求而产生的业务子系统,都应该自研。 原因有二:

  1. 目标用户群体是我们商业闭环的价值源泉,围绕他们的所有工作都是核心业务
  2. 外包团队不会对我们目标用户群体产生同理心——服务目标用户群体的心态只有我们自己的团队才有

外包管理的关键是接口契约:

  • 交付物规格明确:接口定义、行为规格、非功能需求
  • 验收标准清晰:单元测试、集成测试、性能指标
  • 知识转移机制:避免供应商锁定

供应商管理关键要素:

  1. 明确交付物规格(接口契约)
  2. 建立验收标准(单元测试/集成测试)
  3. 持续质量评估(定期回顾)
  4. 知识转移机制(避免供应商锁定)

6.6 典型架构决策案例

  • 自研:用户系统、核心业务逻辑、推荐算法
  • 云服务:对象存储、CDN、数据库(初期)
  • 开源:消息队列、日志收集、监控
  • 外包:客服系统、内部管理工具

Trade-off:开源 vs 云服务

维度开源软件云服务
自主可控
运维成本高——自行维护低——供应商托管
灵活性高——可改源码低——黑盒调用
前期投入中——学习成本低——开箱即用
长期成本低——无授权费高——按用量付费
适用阶段早期 / 有定制需求快速验证 / 非核心能力

Trade-off:外包 vs 自建团队

维度外包自建团队
成本弹性高——按需增减低——固定人力成本
质量可控性中——依赖合同约束高——直接管理
知识积累低——知识在外包团队高——知识在内部积累
响应速度低——需求传递链路长高——直接沟通
适用场景非核心业务、短期需求核心业务、长期迭代

反模式

  • 核心业务外包(外包团队缺乏对目标用户的同理心)
  • 对开源软件放任不管(不跟踪安全漏洞、不定期同步上游、内部 Fork 与上游分歧越来越大)
  • 过度依赖云服务(云服务故障时业务完全不可用、数据迁移困难、长期成本不可控)

七、版本迭代规划

7.1 版本规划是战略级决策

版本规划不是简单的"功能排序",而是需要根据业务发展阶段动态调整战略重心。方向正确并不代表能够走到最后,执行路径和方向同等重要。版本规划可以影响一个业务的成败,一个企业的生死存亡。

7.2 Go 语言版本演进:经典案例

版本时间核心变化战略阶段
Go 1.02012.03兼容性承诺、完善工程工具链从0到1:确立使用范式
Go 1.12013.05性能改善(编译器、GC、map、goroutine调度)从1到100:性能打磨
Go 1.52015.08自举、并发 GC(延迟降一个数量级)、vendor里程碑:解决最大痛点
Go 1.112018.08Go modules、WebAssembly版本管理解决 + 新领域
Go 1.132019.08sync.Pool 改善、逃逸分析重写、GOPROXY性能 + 生态

Go 1.0 之后语言本身的功能基本非常稳定,只有极少量的变动。这些年 Go 都在变化什么?

  1. 性能、性能、性能! 尤其在 GC 效率这块,持续不断优化。为了它大范围重构实现、完成自举
  2. 强化工程能力:各种 Go tool 增加,最突出的是模块版本管理(vendor → modules)
  3. 标准库能力增强:context、HTTP 2.0 等,context 是网络编程最佳实践的标准化
  4. 业务领域扩展:整体专注服务端,对 Android/iOS/WebAssembly 做经验性支持

7.3 业务不同阶段的版本规划策略

图表渲染中…

7.4 从0到1阶段:用户使用姿势是第一位

Go 语言的版本规划给出了最好的示范:它首先把焦点放在用户使用姿势的迭代上。凡与此无关的事情,只要达到及格线了就可以先放一放。这也是为什么 Go 1.0 虽然有很多关于 GC 效率的吐槽,但他们安之若素,仍然专注于使用姿势的迭代。

使用姿势 = 产品设计的规格。如果使用姿势错了,后面的一切都是在错误地基上盖高楼。

Go 1.0 发布的兼容性承诺是 Go 语言发展的里程碑:承诺未来版本将保持向后兼容,保证已有代码在新版本下编译和运行的正确性。使用姿势一旦稳定就锁定,新功能只能在旁路演化。

7.5 从1到100阶段:非功能性需求成为战略焦点

一旦语言开始大规模推广,进入扩张阶段,版本迭代的关注点切换到用户看不见的地方——非功能性需求。生产环境中用户最关心的指标,就成了团队最关注的事情,日复一日不断迭代优化。

这是很了不起的战略定力:知道什么情况下,最该做的事情是什么。

7.6 重大冲击需求:谨慎处理,旁路演化

遇到会对产品产生巨大冲击的功能需求(如 Go 的泛型),正确的姿势是:

  1. 认真对待:Go 团队非常认真地对待泛型需求
  2. 不急于实现:并没有急着在 1.9、1.10 或 1.13 中加入
  3. 旁路独立演化:泛型被放到旁路版本独立演化,直到验证成熟才会合并到 1.x
  4. 灰度验证:在少量客户上先做灰度

尊重客户的正确姿势是:别折腾他们。

7.7 优先级排序技术

技术说明适用场景
MoSCoWMust have / Should have / Could have / Won't have需求分类、版本范围确定
WSJF(加权最短作业优先)(用户价值+时间价值+风险降低) / 作业规模敏捷开发中的特性排序
Kano 模型基本型 / 期望型 / 兴奋型需求产品功能规划
RICE(Reach × Impact × Confidence) / Effort产品经理排序

7.8 最小可行产品(MVP)

MVP 的核心思想是用最小的成本验证最大的市场假设。与"从0到1阶段聚焦用户使用姿势"的理念完全一致:

  • MVP 不是粗糙的产品,而是精简但完整的产品
  • MVP 的目标是验证使用姿势(产品设计规格)是否正确
  • 验证成功后进入从1到100阶段,迭代非功能性需求

深度注记:Go 版本管理的演进说明版本规划不排斥推翻重来——vendor 不成功就推翻,module 是更好的方案。核心痛点(版本管理是 Go 1.0 后社区调查排名第一的痛点)必须持续攻坚。这也是"从1到100阶段,工程能力是战略焦点"的体现。

Trade-off:使用姿势 vs 性能

阶段优先级理由
从0到1使用姿势错的使用姿势 = 错的产品方向,性能再好也无意义
从1到100性能/稳定性使用姿势已稳定,竞争力在非功能性需求

Trade-off:快速响应 vs 稳定演进

  • 快速响应重大需求:满足客户期望,但可能打乱迭代节奏、引入不兼容变更
  • 稳定演进旁路演化:保护既有客户,但短期可能引起社区不满
  • 最佳实践:重大冲击需求放旁路独立演化,成熟后再合并。回到从0到1方法论,在少量客户上先做灰度

反模式

  • 技术背景的版本规划者一开始就陷入技术细节泥潭(首先应确定焦点放在哪里)
  • 扩张阶段忽视非功能性需求(产品竞争力不足,被竞品超越)
  • 急于响应重大冲击需求(引入不兼容变更,折腾所有既有用户)

八、软件工程的未来

8.1 三个演进方向

图表渲染中…

方向一:设计范式的沉淀。软件工程是高度设计驱动的工程活动,设计范式的沉淀是走向成熟的第一个标志:

层次说明成熟度
架构范式MVC、微服务、事件驱动、CQRS较成熟
设计模式GoF 23 种设计模式成熟
并发范式Actor、CSP、响应式较成熟
数据范式关系型、文档型、时序型、图较成熟
业务范式领域驱动设计、Event Sourcing发展中
AI 辅助设计LLM 辅助架构决策、代码生成萌芽期

关键判断:设计范式的沉淀不是让设计变成机械化的套公式,而是让"套路"成为默认起点,把创造力释放到真正需要创新的地方。类比围棋的定式——不是让对局机械化,而是让棋手不必在每一个局部重新发明。

方向二:工程手段的确定性提升。从版本管理(go mod)到质量管理(单元测试、CI/CD、灰度发布),到共识管理(接口定义、代码即文档),到容器化(镜像级别确定性),工程手段的确定性提升贯穿了软件工程的全过程。

方向三:基础能力的云化。IaaS 层已完全云化,PaaS 层正在进行,SaaS 层在进行中,AI 能力层刚起步。云化深化不是全盘云化——核心业务逻辑和合规敏感数据始终需要自研和私有化。

8.2 AI 对软件工程的影响

AI 在软件工程中的影响谱系:

影响领域具体表现影响程度
代码生成Copilot / Cursor编码效率大幅提升
设计辅助架构方案推荐辅助探索,无法替代战略判断
测试生成自动化测试案例覆盖率策略仍需人类定义
代码审查智能 Review提高审查效率,但不能完全替代
运维智能异常检测/自愈提升线上稳定性

AI 辅助不会替代架构师,但会改变架构师的工作

  • 编码层面:大幅提升编码效率,但需要人类审查
  • 设计层面:辅助方案探索,但无法替代战略判断
  • 质量层面:自动化测试生成,但覆盖率策略需人类定义

架构师的核心竞争力不是写代码的速度,而是对业务方向的判断力、对权衡取舍的决策力、对团队协同的影响力——这些能力在 AI 时代反而更加重要。

8.3 软件工程的成熟标志

图表渲染中…

当设计范式足够丰富、工程手段足够确定、基础能力足够标准化时,软件工程将从"手艺"真正走向"工程"。但这条路还很长。

8.4 新兴趋势

趋势说明与核心矛盾的关系
Platform Engineering内部开发者平台,降低认知负载为开发者注入确定性
DevSecOps安全左移,安全内建于开发流程早期发现安全不确定性
Observability-Driven Development基于可观测性驱动的开发运行时不确定性可视化
Dev 与 Ops 的融合开发与运维的边界消融降低部署不确定性
Low-Code / No-Code降低编码门槛将部分设计范式固化到平台

8.5 核心矛盾不会消失

无论技术如何进步,软件工程的核心矛盾——不确定性 vs 确定性——不会消失。AI 和云化是缓解手段,不是终极解决方案。忽视这一本质会导致:

  • 过度依赖工具,丧失架构判断力
  • 过度追求自动化,忽视人为审查的价值
  • 过度追求标准化,扼杀创新

深度注记:严谨并非创新的对立面,而是创新的重要基础。每个人都有灵光乍现的时刻,但是唯有那些拥有严谨的科学态度的人才能抓住它,把它变成现实。这句话,是理解软件工程未来的钥匙。


总结

全篇知识体系总结表

主题核心原则关键实践反模式
宏观视角在不确定性中寻找确定性确定性注入手段(版本管理、自动化测试、CI、代码审查)用管理传统工程的方式管理软件工程
设计文档规格高于实现,设计是头等大事四段式结构(现状/需求/满足方式/实现原理),接口先行架构图即共识,文档写完即弃
代码阅读先文档后代码,概要设计是首要目标工具提取实体→example/test→推测→证实/证伪→形成文档从零开始反向推导,不形成文档
发布与版本管理只读设计提升确定性语义化版本,容器化终极方案,灰度发布破坏版本只读语义,忽视环境不确定性
质量管理测试代码与功能代码同等重要单元测试重中之重,高频交付,CI/CDCheck List 管理质量,发布是特殊事件
开源与外包核心自研,非核心外包开源需治理,云服务关注 SLA,外包关键是接口契约核心业务外包,对开源放任不管
迭代规划不同阶段侧重点极大不同从0到1用姿势,从1到100磨性能,重大需求旁路演化陷入技术细节,忽视非功能性需求
未来展望核心矛盾不会消失设计范式沉淀,工程确定性提升,基础能力云化AI 代码不经审查,过度追求范式导致僵化

软件工程六大核心原则

原则本质
在不确定性中寻找确定性软件工程的第一性原理
共识即契约,规格高于实现团队协同的精确性要求
设计是头等大事开发期投入1分,维护期回报百倍
只读设计提升确定性版本基线的确定性预期
高频交付优于低频大版本应对不确定性的最佳方式
核心自研,非核心外包竞争优势的边界意识

高效团队 vs 低效团队

维度高效团队低效团队
共识精确无歧义(接口定义级别)模糊(架构图代替接口定义)
文档与代码一致,相互印证脱节,文档过时无人更新
质量内建(单元测试是成果,CI/CD 是习惯)外挂(测试是上线前临时活动)
架构持续优异(团队共同坚持)持续腐化(缺乏守护)
新人融入快(文档清晰、代码可读、默契度高)慢(靠"口口相传"传承知识)

协同的科学是团队效率的根因——团队与团队的效率差距可达百倍以上,根源不在技术,而在共识管理、知识传承、默契程度。


思考题

  1. 如果你接手一个没有文档的遗留系统,第一步会做什么?如何平衡"理解系统"和"交付需求"的时间分配?
  2. 在你的项目中,团队共识主要靠什么载体?口头约定、文档、还是接口定义?共识的精确度够吗?
  3. 为什么"发布频率越高,单次风险越低"这个结论看似反直觉?你们团队的发布频率是多少?有哪些瓶颈阻止了更高频的发布?
  4. 一个模块从"自研"转为"云服务"依赖,或者从"云服务"转回"自研",分别需要考虑哪些因素?
  5. Go 1.0 发布时 GC 性能被广泛吐槽,但团队安之若素继续迭代使用姿势。如果换作你的项目,你能承受这种战略定力吗?
  6. AI 生成代码不经审查直接使用,与"阅读代码可以顺手改代码"的原则是否矛盾?如何平衡 AI 辅助效率与质量保障?

关联阅读

  • 68讲:软件工程的宏观视角(核心矛盾与架构师职责)
  • 69讲:团队的共识管理(共识五层次与契约即共识)
  • 70讲:怎么写设计文档(四要素与多方案对比)
  • 71讲:如何阅读别人的代码(架构反向工程)
  • 72讲:发布单元与版本管理(只读设计与容器化)
  • 73讲:软件质量管理(单元测试、CI/CD、高频交付)
  • 74讲:开源、云服务与外包管理(核心自研与接口契约)
  • 75讲:软件版本迭代的规划(从0到1用姿势、从1到100磨性能)
  • 76讲:软件工程的未来(范式沉淀、工程确定性、云化)
  • 77讲:软件工程篇回顾与总结(六大核心原则与协同科学)
  • 49讲:发布、升级与版本管理(灰度发布与回滚策略)
  • 50讲:架构实战:架构设计文档模板(备选方案模板与架构设计模板)
  • 51讲:如何画出优秀的软件系统架构图(4R架构定义与常见架构图类型)

延伸视角

从系统工程看软件工程:软件工程的"不确定性 vs 确定性"矛盾,本质上是复杂适应系统的特征——系统的行为不能从组成部分的行为简单推导。这意味着传统的还原论方法(分而治之)在软件工程中必须辅以整体论视角——模块拆分后,必须关注模块间的协作语义(接口契约)。

从认知科学看设计文档:设计文档本质上是团队共享心智模型(Shared Mental Model)的外化。认知科学研究表明,共享心智模型的精确度直接影响团队绩效。这就是为什么"精确的接口定义远胜冗长的文字描述"——前者减少了认知加工的模糊性。

从经济学看"设计是头等大事":开发期投入1分、维护期回报百倍,这不是线性关系而是复利效应——良好的设计让每次迭代都站在坚实的基础上,而糟糕的设计让每次迭代都在偿还技术债。技术债的利息是复利计算的,这是很多团队"越做越慢"的根本原因。

从组织理论看"核心自研":核心业务必须自研,不仅是技术问题,更是组织能力问题。核心业务的知识积累是组织的动态能力(Dynamic Capability),它决定了组织在快速变化环境中的适应能力。将核心业务外包,本质上是将动态能力外包——这在战略上是不可持续的。

从演化论看版本迭代:Go 语言的版本演进完美诠释了"渐进式演化"——每一代都在上一代的基础上做微小但方向明确的改进。泛型的旁路演化策略,类似于生物学中的"分支演化"——新的能力在旁系中独立演化,待成熟后再与主干合并。这种策略既保证了主干的稳定性,又不妨碍创新探索。