加载中...

本篇要回答的问题:软件工程是 “无数个项目” 的反复迭代,如何用版本管理保证质量?什么是 “发布单元”?只读设计为什么是版本管理乃至整个软件工程的灵魂?

上一讲谈了 “如何阅读别人的代码”——那是理解既有系统。本讲转向如何让长生命周期、反复迭代的系统保持质量确定性。软件工程不是一个项目,而是无数个项目,每个项目只是其中的一个里程碑(Milestone),这种反复迭代的工程要保质极难。

为什么需要版本管理

最朴素的思路:出问题了就召回、换老版本。

需求层次 要管什么
仅为召回 只需对可执行程序做版本管理
进一步定位问题、稳定再现 必须对源代码也做版本管理,且与可执行程序版本一一对应

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

软件分模块开发,不同模块可能由不同团队甚至第三方开发。从细粒度看,一个软件工程包含很多个彼此独立的子软件工程,它们有自己的迭代周期,你只是它们的 “客户”。这种实体称为发布单元——直观特征是它有自己独立的源代码仓库(repo)。

关键判断:发布单元你可能直觉以为就是模块,但两者有很大不同——发布单元的核心是 “独立的迭代周期”。

发布单元的输出不一定是可执行程序:

输出形态 说明
可执行程序 / 虚拟机字节码 最终成品
动态库 so / dylib / dll
虚拟机动态库 如 JVM 的 jar 包
静态库(.a) 可执行程序的半成品,由链接器组装成成品
源代码本身 部分语言主张源代码发布,如 Go

发布单元的输入两部分:① 自己独立演进的模块(repo 托管代码);② 自己依赖的发布单元列表(外部、有独立迭代周期)。

源代码版本管理:以 GitHub 为例

源代码仓库系统(svn/git)一般只能管到输入的第一部分。GitHub 提供的质量管理手段:

手段 作用
开发活动独立性 低成本建 branch,一个分支做一个 feature,未完成对他人不可见,并行开发互不干扰
代码质量检查机制 提 pull request 合并前检查:单元测试、覆盖率(code coverage)、静态检查(lint)、人工互审(code review)……需求易变,故 GitHub 做了开放设计(再次感受开闭原则威力)
完善的回滚(revert) 已合并的有 Bug 的 PR 可 revert,主干得到去除该功能的新发行版

外部依赖管理与"只读"前提

依赖管理的目标:指定本发布单元依赖的各发布单元的建议版本,从而稳定持续地从源代码构建出相同能力的输出(Go 早期社区方案林立,最终 go mod 统一)。

核心前提:一个打好版本号的发布单元是只读的,不能做任何改动。

只读语义包含两层,破坏其一就会连锁破坏所有依赖者:

只读约束 含义
不改自身模块代码 容易理解
不改依赖的外部发布单元版本 如把 opencv 从 v1.0 升到 v2.0 也是一次变更,必须改自己的版本号

但仅源代码 + 依赖只读,仍不足以保证输出确定性,还有两个东西没只读:

未只读的东西 为何不纳入版本管理
操作系统内核 大部分情况质量有保证,行为不一致被记为软件 Bug 而非改操作系统
编译器 同理,理论上不同版本编译结果可能行为不同

软件发布的版本管理:容器化

源代码构建(build)是相对封闭可预期的环境(甚至可规定 OS 种类版本、编译器版本)。但软件发布环境千差万别——apt / rpm / brew 安装常失败,根因是用户系统环境差异太大,让发布者适配所有环境是过高要求。

彻底解决之道:容器化。容器镜像(image)不只含可执行程序,还完整包含运行所需的全部环境(动态库、运行时、甚至依赖的 “操作系统”),实现完全自描述、不依赖任何外部环境。新版本有缺陷?回滚到老镜像即可。

只读设计的确定性(贯穿全篇的哲学)

版本是一个 “基线”,只读让我们对它有确定性预期——这正是软件工程的核心:在大量不确定性中找确定性。只读思想被广泛运用:

场景 只读如何体现
版本管理 打号的发布单元只读
开闭原则 模块的业务范畴只读,接口稳定,模块才能自由组合
函数式编程 倾向变量只读,提高确定性预期
大数据 Spark / RDD RDD 只读,施加 transform 得到新 RDD,让重试、延迟计算、缓存都极其简单

版本的兼容问题

依赖特定版本解决了确定性,但总会想升级新版本。最大风险是所依赖模块完成了一次重构——主体功能好兼容,难度全在细节(错误码、低频分支行为)。

兼容策略 适用 / 代价
放弃兼容,连函数名都改 干脆:客户升级后编译不过,老实用新接口重写重测
必须兼容(互联网 API) 发布的 api 难收回(会丢大量客户),故为 api 引入版本号 /v2/foo/bar,不兼容修改时升到 /v3/foo/bar

关键判断:api 加版本号的额外好处——全局重构且难兼容细节时,可直接升版本号,线上保留两个版本并存(nginx 作为分派网关)。但这是不得已的办法,能兼容还是尽量兼容,否则客户端升级的心智负担越大。

总结

复杂软件总可分割为若干独立迭代的发布单元以分而治之。发布单元的切割不宜过细,以一个小团队负责起来比较舒服为宜。源代码用 git/GitHub 管开发期质量,发布用容器镜像管运行期确定性,只读设计提供确定性基线,而版本兼容是最容易踩的坑。

先把"是什么"回答清楚

概念 一句话说明
发布单元 拥有独立迭代周期、独立 repo 的软件实体(≠ 模块)
只读语义 打号的发布单元不可改自身代码、也不可改依赖版本
容器镜像 自描述、含全部运行环境的版本单元,比源代码版本管理更进一步
api 版本号 /v2/.../v3/...,应对不兼容修改,可线上多版本并存

一句话速记

把系统拆成独立迭代的 “发布单元”,给每个打上只读的版本基线,再用容器镜像把运行环境一起冻结——用 “只读” 在反复迭代的不确定性里凿出确定性。

几条值得记住的判断

  • 只读 = 确定性:从版本、开闭原则到 Spark RDD,本质都是只读思想。
  • 容器化才彻底解决软件发布在异构环境下的安装失败问题。
  • 兼容的难度全在细节(错误码、低频分支);互联网 API 用版本号 + 多版本并存兜底。

思考题

回到你的项目:你依赖的外部模块都锁定版本号了吗,还是有人偷偷把某个依赖 “原地” 升级却没改自己的版本号,破坏了只读语义?你对外提供的 API,下一次不兼容改动打算靠 “改名硬断” 还是 “升版本号并存”?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关