跳到主要内容

可演进的架构:与复杂性讨价还价

这一章讲三件事: 复杂性是什么、藏在哪、为什么消除不了; 什么样的代码和接口让「明天的修改」变便宜; 以及同一个问题在数据上的重演:schema、迁移与下游兼容。 读完你会有一套「这次改动会不会让系统更难改」的自检法。

1. 这一章讲什么

需求的不确定性是软件项目躲不掉的挑战:客户需求和环境随时间变化,应用必须跟着变。管理者用迭代(一小段一小段地做、每段结束调整方向)开发(第 11 章)来吸收不确定性;而工程师这边的答案,原书一句话:构建可演进的架构,因为复杂性是演进性的敌人1

本章在全书链条里的位置:第 09 章讲了怎么「做出」设计;本章讲怎么让设计出来的东西活得久——它是第 04 章(版本号承诺)与第 07 章(展开策略)在架构层的会师点。

2. 顶层全景

明天的需求今天不知道

├─ 敌人:复杂性(难以理解和修改)
│ 三症状: 高依赖(牵一发动全身) / 高隐蔽(看不出改动的波及) / 高惯性(改不动)
│ 定律: 复杂性无法消除,只能选择放在哪里
├─ 代码层三原则: YAGNI(别造没用的) / 最小惊讶(别埋隐性知识) / 领域封装(把爆炸半径圈小)
├─ 接口层: 兼容性——新旧版本能否共存(Greeter 三次演进)
└─ 数据层: 同样的问题换了个战场——schema / 迁移工具 / 下游(ETL/CDC)兼容

图说:自上而下从「意识」到「纪律」;主走查在 3.3(Greeter 演进)。

3. 核心原理

3.1 认清敌人:复杂性的三个症状

原书借约翰·奥斯特霍特《软件设计的哲学》的定义起步:复杂性是「与系统结构有关的东西」——它使人难以理解和修改系统2。奥斯特霍特给了两个症状,原书加了第三个:

  • 高依赖性:代码与别的 API 或行为绑在一起。依赖本身不可避免甚至可取,但每个新连接都让代码更难改;它带来紧耦合(模块之间咬得太死)和变更放大(改一处要跟着改一串)3;
  • 高隐蔽性:你无法预测一次改动的副作用。「知道得太多的对象、鼓励副作用的全局状态、过度的间接寻址」都是它的症状——晦涩的代码需要更长的时间学习,也更容易被无意破坏4;
  • 高惯性(作者加的):软件保持着旧的使用习惯,难以调头。给实验写的、随时可丢弃的代码惯性低;驱动着十几个关键业务应用的服务惯性高。由此得到一条花力气的判据:高惯性、高变化的系统才值得花大力气简化;低惯性或低变化的系统,复杂一点可以忍5

然后是本章最重要的一句话,值得单独抄:复杂性不总能被消除,但你可以选择把它放在哪里6。向后兼容的代码用起来简单、实现起来复杂;解耦用的间接层降低依赖、却抬高隐蔽性——每一笔都是交易,想清楚再付。

3.2 代码层三原则

原则一:YAGNI(You Ain't Gonna Need It,你不是真的需要它)。 面对未知需求,工程师爱玩两种把戏:猜未来、或提前造「抽象逃生舱门」——原书直接说别玩,两种都会提高复杂性;保持简单(KISS 原则),等需求真清晰了再加复杂度7。YAGNI 的三大违规现场:过早优化(没人用之前就上缓存、分库、队列——优化没被证明需要,复杂度却永久留下);过度灵活的抽象;MVP(最小可行产品,能拿去换用户反馈的最小功能集)之外「顺手」加的特性8

抽象的代价有一个具体形状。原书给了一个分布式队列接口:看起来极简,只有 send 和 receive 两个方法9:

interface IDistributedQueue {
void send(String queue, Object message);
Object receive(String queue);
}

一旦底层换成了 Kafka(消息带键)或亚马逊 SQS(消息要确认 ACK),这个接口就逼你二选一:做所有特性的并集(得到一个没有任何实现能完整满足的接口)或所有特性的交集(穷得没法用)。原书的裁决:不如直接用某个具体实现,真要换的时候再重构10

YAGNI 配两个减压阀:蒙茨法(muntzing,反着问——对手里每样东西问「真的需要吗」,其余全扔)让你的软件保持苗条11;以及「接口填充程序」——真怀疑未来要加压缩或加密时,可以在文件格式头里预留一个编码字段但只实现未压缩,将来加算法时旧代码仍能读旧文件:留门,不盖楼12

原则二:最小惊讶。 接口的行为要像用户最初预期的那样;惊讶就是复杂性的种子。原书点了两类「隐性知识」违规——凡是调用者必须知道、却没写在接口里的知识13:

  • 隐藏的排序需求:「方法 A 必须先于方法 B 调用」却不在类型上体现。原书给的修法很具体:让方法自己调子方法(pontoonWorples() 开头先检查 if(!flubberized) flubberize())、合并方法、用构建者模式或类型系统——最不济,把方法改名叫 pontoonFlubberizedWorples,把知识写进名字里14;
  • 隐藏的参数需求:签名声称收 int,实际只接受 1 到 10。修法:用能精确表达约束的类型、JSON 字段用 schema 描述;实在不行写进文档15

原则三:封装领域知识。 按业务领域(会计、计费、运输)而不是技术层(前端/中间层/后端)来组织代码,让「同一个业务的变化」聚集在同一个模块里。这样的代码天然高内聚、低耦合——相关的代码住在一起,彼此独立,变更的「爆炸半径」(一次改动波及的范围)就小16。原书特意点了技术分层的坑:按层分代码,一项业务逻辑的变化要穿过所有层;不同层的团队之间还要协调17。把业务概念对应到软件上,有一整套方法论叫领域驱动设计(DDD)——原书的建议是了解它,但只在最复杂的情况下全面使用18

3.3 主走查:一个问候服务的三次演进

兼容性是本章的核心技术。先定义(方向感别丢):向前兼容 = 旧服务能接新客户端的请求;向后兼容 = 新服务不破坏旧客户端19。然后拿原书的 gRPC 例子(用协议缓冲区 Protocol Buffers 定义的服务,字段按序号而不是名字传输)走三次演进20:

第 0 版
message HelloRequest { string name = 1; int32 favorite_number = 2; }

演进一:加必填邮箱 → 向后不兼容 ✗
message HelloRequest { ... required string email = 3; }
走查:旧客户端(不知道 email 字段)调用新服务 → email 缺失
→ 服务端解析失败。
(required = 必须提供;不带值就无法解析)
修法:去掉 required、缺了就跳过 → 恢复向后兼容。

演进二:int32 换成 sint32(支持负数的变体,编码方式不同)→ 双向不兼容 ✗✗
走查:新客户端用 sint32 的编码发 favorite_number → 旧服务解析不了;
旧客户端用 int32 的编码发 → 新服务解析不了。
两个方向全断——类型变更比加字段危险得多。
修法:不动原字段,新增一个字段。

第三次演进(正确姿势):保留旧字段、新增字段,靠字段序号区分21:

message HelloRequest {
string name = 1;
int32 _deprecated_favorite_number = 2; ← 旧字段,序号不动
sint32 favorite_number = 3; ← 新字段,新序号
}
走查:旧客户端仍填字段 2 → 新服务读它(或忽略,或做转换);
新客户端填字段 3 → 旧服务忽略它(不认识就跳过)。
两个方向都活着。监控字段 2 的使用率,客户端升级完再清理。

这一节的后坐力来自一条行业教训:必填字段「必填项永远存在」——一旦有人没填,你永远不能撤掉处理缺值的代码。原书引 Protocol Buffers v2 主要作者肯顿·瓦尔达的话:required 关键字被证明是「一个可怕的错误」,以至于它在 v3 里被整个移除22

版本化是兼容性的兜底。 兼容做得再好,终有不兼容的那天(比如一个必填的新字段)。版本化你的 API 意味着引入新版本时旧版本继续服务:请求被 API 网关按版本路由,v2 的流量进 v2.x.x 的实例23。版本化有代价——旧版本要维护、bug 修复要回传——所以原书提醒务实:对外部(你控制不了的)客户端,版本化最重要;服务端和客户端都在自己手里时,内部 API 也许不需要24

3.4 数据层:同一个故事,更长的余波

代码改完就完事,数据却要一直活着。原书点破关系:API 比持久化数据短命——客户端和服务端都升级完,API 的使命就结束了;数据却要随应用一起演进25。于是同样的问题在数据上重演,配方也类似:

隔离。 共享数据库(多个应用共读共写一个库)会导致丧失自主性——你必须优先考虑别人怎么用,否则改不了 schema;还有「同一个字段各家理解不一样」的分歧、性能互相拖累、安全边界穿洞26。隔离的库只有一个读写者,其余流量走 RPC——改 schema 时你只需要担心自己的应用27

显式 schema。 「无 schema」数据存储的流行让人以为结构是负担;原书的立场很硬:无模式不等于没有模式——数据总有一种隐含的模式,只是它躲在读取时才能推断的地方28。代价是原书给的「大杂烩」例,三行真实形状的数据29:

{"name": "Fred", "location": [37.33, -122.03], "enabled": true}
{"name": "Li", "enabled": "false"} ← enabled 是字符串?
{"name": "Willa", "location": "Boston, MA", "enabled": 0} ← 又是数字?

三个月后没人说得清 enabled 是什么类型、location 是坐标还是城市名。对照一条显式 schema(SQL 建表语句),写入时就能挡住畸形数据,数据科学家也终于可以放心解析30。原书同样点了一类反面:「把 JSON 字符串塞进叫 data 的字段」是自取灭亡——显式 schema 的痛苦你一分没少,收益一点没拿31。例外也照实写了:快速试错期、旧数据没价值时,无模式少走弯路——和 YAGNI 是同一条逻辑。

自动化迁移。 手动在数据库上执行 DDL(数据描述语言,如 ALTER TABLE)很容易出错。迁移工具(原书演示 Liquibase)把每一次 schema 变更写成带版本号、可回滚的脚本文件,进版本控制、走评审32:

--changeset criccomini:create-users-table
CREATE TABLE users ( ... );
--rollback DROP TABLE users
--changeset dryaboy:add-email
ALTER TABLE users ADD email VARCHAR(255);
--rollback DROP COLUMN email

三条纪律:工具会在数据库里建自己的元数据(关于数据本身的数据)表——看到 DATABASECHANGELOG 不要惊讶33;schema 迁移与应用部署解耦——别把数据库变更绑进应用发布,变更时机要自己可控34;以及回滚是有限的——「回滚删除的列」会重建那列,但不会复活曾经存在里面的数据,所以删除前先备份整表35

下游兼容。 数据的「客户端」藏在暗处:ETL(抽取-转换-装载,把生产库数据搬运到分析用数据仓库的管道)依赖你的 schema,删一列可能让整条管道停摆36;CDC(变更数据捕获,把每一次插入/更新/删除变成事件消息的架构)里,「members 表的插入」可能就是一封欢迎邮件的触发器——这些消息是一种隐含的 API,不兼容的 schema 变更直接打断别人的服务37。防护两招:变更前跑兼容性检查(提交时查 DDL、集成环境里验证);以及发布数据产品——把内部 schema 对应转换成一个独立的、面向消费者的对外 schema,内部随便演进,对外只承诺兼容38

4. 作者的判断与证据

  • 「高惯性」是作者自己加的第三个症状:原书明确注明「我们要再加上第三个」——这是作者的延伸,不是奥斯特霍特的原典;引用时别张冠李戴。
  • 「无模式是自取灭亡」「显式 schema 优于无模式」:作者立场,论证是数据完整性与隐蔽性的机制推演,加一个三行 JSON 的实例;作者同时诚实列出了例外场景。
  • required 的教训:引用 Protocol Buffers v2 主要作者的原话与 v3 移除 required 的事实——本章最硬的外部证据。
  • Greeter 三次演进:原书自带的示范代码,主走查完全沿用;兼容性判定是规范性行为,不是作者观点。
  • 蒙茨法:来自硅谷工程师 Bob Munz 的轶事方法,原书当实用技巧转述。

5. 边界与局限

  • 「不写抽象」有反对派:本章反「为未来抽象」,但泛型、插件机制在框架类软件里是产品本身;YAGNI 的适用面是业务应用,不是所有软件。
  • 单体 vs 微服务的路线之争只字未提:本章的「隔离数据库」「领域封装」暗合服务化方向,但没有展开服务拆分的判据。
  • 数据层只到仓库为止:流处理、数据湖、湖仓一体等现代形态不在书里;CDC 的典型实现(如 Debezium)也没点名。
  • 兼容性判定以 Protocol Buffers 为例——JSON/REST 场景的兼容判定(缺字段、类型宽窄)规则略有不同,原书只有一句话带过。

6. 可带走的

  1. 复杂性三症状自查:改动要联动几处(依赖)?能否预测波及(隐蔽)?这块代码还在高速变化吗(惯性)?
  2. 复杂性是预算:不能不花,但要选地方花——「用起来简单」和「实现简单」经常二选一;
  3. 别赌未来:既不预判需求,也不造抽象逃生舱;留门(接口填充程序),不盖楼;
  4. 接口不许埋隐性知识:排序需求让方法自己搞定,参数需求让类型表达;
  5. 按业务领域组织代码,把爆炸半径圈小;技术分层适合小系统,业务一多就乱;
  6. 兼容性口诀:加可选字段安全,改类型双向断,required 是「永远存在」的债;
  7. 演进 API 的标准动作:旧字段保留、新字段加序号、监控旧字段用量、择期清理;
  8. 无模式数据的隐含 schema 是定时炸弹;显式 schema 是写给三个月后的自己的文档;
  9. schema 变更走迁移工具、进版本控制;迁移与应用部署解耦;回滚救不了被删的数据,删前备份;
  10. 下游有暗客户端:ETL 管道和 CDC 消息都是你的 API,对外用数据产品隔一层。

7. 原文地图

主题原书章原文位置
复杂性定义第11章 构建可演进的架构text/20-ch11.txt:21(搜「难以理解和修改系统」)
三症状第11章 构建可演进的架构text/20-ch11.txt:23(搜「高依赖性和高隐蔽性」) · text/20-ch11.txt:24(搜「高惯性」)
变更放大第11章 构建可演进的架构text/20-ch11.txt:28(搜「变更放大」)
高隐蔽症状第11章 构建可演进的架构text/20-ch11.txt:33(搜「副作用」)
惯性判据第11章 构建可演进的架构text/20-ch11.txt:40(搜「新特点」)
复杂性放哪里第11章 构建可演进的架构text/20-ch11.txt:46(搜「把它放在哪里」)
两种把戏与 KISS第11章 构建可演进的架构text/20-ch11.txt:56(搜「逃生舱门」) · text/20-ch11.txt:58(搜「KISS」)
YAGNI第11章 构建可演进的架构text/20-ch11.txt:63(搜「You ain」) · text/20-ch11.txt:79(搜「过早优化是指」)
队列接口两难第11章 构建可演进的架构text/20-ch11.txt:97(搜「IDistributedQueue」) · text/20-ch11.txt:106(搜「特性的并集」)
蒙茨法第11章 构建可演进的架构text/20-ch11.txt:114(搜「蒙茨法」)
接口填充程序第11章 构建可演进的架构text/20-ch11.txt:125(搜「接口填充程序」)
最小惊讶第11章 构建可演进的架构text/20-ch11.txt:133(搜「最小惊讶原则」) · text/20-ch11.txt:141(搜「隐藏的排序需求」) · text/20-ch11.txt:168(搜「隐藏的参数需求」)
排序需求修法第11章 构建可演进的架构text/20-ch11.txt:151(搜「pontoonWorples」) · text/20-ch11.txt:163(搜「pontoonFlubberizedWorples」)
领域封装第11章 构建可演进的架构text/20-ch11.txt:187(搜「高内聚和低耦合」) · text/20-ch11.txt:188(搜「爆炸半径」) · text/20-ch11.txt:192(搜「前端、中间层和后」)
DDD第11章 构建可演进的架构text/20-ch11.txt:200(搜「domain-driven design」)
API 小巧第11章 构建可演进的架构text/20-ch11.txt:218(搜「小巧的 API」) · text/20-ch11.txt:219(搜「认知负担」)
IDL第11章 构建可演进的架构text/20-ch11.txt:239(搜「interface definition language」)
兼容定义第11章 构建可演进的架构text/20-ch11.txt:259(搜「向前兼容的变更」) · text/20-ch11.txt:262(搜「向后兼容的变更则恰恰相反」)
主走查:Greeter第11章 构建可演进的架构text/20-ch11.txt:271(搜「SayHello」) · text/20-ch11.txt:298(搜「required string email」) · text/20-ch11.txt:301(搜「向后不兼容的变更」)
sint32 双向断第11章 构建可演进的架构text/20-ch11.txt:324(搜「sint32 favorite_number」) · text/20-ch11.txt:327(搜「既不向前兼容也不向后兼容」)
新字段演进第11章 构建可演进的架构text/20-ch11.txt:331(搜「增加一个新的字段」) · text/20-ch11.txt:344(搜「废弃字」)
required 教训第11章 构建可演进的架构text/20-ch11.txt:307(搜「可怕的」) · text/20-ch11.txt:311(搜「必填项永远存在」)
API 版本化第11章 构建可演进的架构text/20-ch11.txt:361(搜「版本化你的 API」) · text/20-ch11.txt:365(搜「API 网关或服务网格」)
数据库隔离第11章 构建可演进的架构text/20-ch11.txt:406(搜「丧失自主性」)
无模式真相第11章 构建可演进的架构text/20-ch11.txt:454(搜「无模式并不意味着」) · text/20-ch11.txt:465(搜「大杂烩」)
大杂烩例第11章 构建可演进的架构text/20-ch11.txt:476(搜「Willa」)
显式 schema第11章 构建可演进的架构text/20-ch11.txt:486(搜「CREATE TABLE users」)
自取灭亡第11章 构建可演进的架构text/20-ch11.txt:502(搜「自取灭亡」)
Liquibase第11章 构建可演进的架构text/20-ch11.txt:537(搜「liquibase formatted sql」) · text/20-ch11.txt:553(搜「add-email」)
元数据表第11章 构建可演进的架构text/20-ch11.txt:570(搜「DATABASECHANGELOG」)
部署解耦第11章 构建可演进的架构text/20-ch11.txt:575(搜「生命周期联系在一起」)
回滚有限第11章 构建可演进的架构text/20-ch11.txt:594(搜「不会重新创建那些曾经存储」)
ETL第11章 构建可演进的架构text/20-ch11.txt:619(搜「extract transformation load」)
CDC第11章 构建可演进的架构text/20-ch11.txt:632(搜「change data capture」) · text/20-ch11.txt:635(搜「隐含的 API」)
数据产品第11章 构建可演进的架构text/20-ch11.txt:644(搜「显式解耦的数据产」)

Footnotes

  1. 出处:「第11章 构建可演进的架构」第 10 段(text/20-ch11.txt:10,搜「演进性的敌人」)。

  2. 出处:「第11章 构建可演进的架构」第 21 段(text/20-ch11.txt:21,搜「难以理解和修改系统」)。

  3. 出处:「第11章 构建可演进的架构」第 28 段(text/20-ch11.txt:28,搜「变更放大」)。

  4. 出处:「第11章 构建可演进的架构」第 33 段(text/20-ch11.txt:33,搜「副作用」)与第 38 段(text/20-ch11.txt:38,搜「远距离操作」)。

  5. 出处:「第11章 构建可演进的架构」第 40 段(text/20-ch11.txt:40,搜「新特点」)与第 44 段(text/20-ch11.txt:44,搜「保持复杂」)。

  6. 出处:「第11章 构建可演进的架构」第 46 段(text/20-ch11.txt:46,搜「把它放在哪里」)。

  7. 出处:「第11章 构建可演进的架构」第 56 段(text/20-ch11.txt:56,搜「逃生舱门」)与第 58 段(text/20-ch11.txt:58,搜「KISS」)。

  8. 出处:「第11章 构建可演进的架构」第 79 段(text/20-ch11.txt:79,搜「过早优化是指」)与第 77 段(text/20-ch11.txt:77,搜「minimum viable product」)。

  9. 出处:「第11章 构建可演进的架构」第 97 段(text/20-ch11.txt:97,搜「IDistributedQueue」)。

  10. 出处:「第11章 构建可演进的架构」第 106 段(text/20-ch11.txt:106,搜「特性的并集」)与第 109 段(text/20-ch11.txt:109,搜「以后进行重构」)。

  11. 出处:「第11章 构建可演进的架构」第 114 段(text/20-ch11.txt:114,搜「蒙茨法」)。

  12. 出处:「第11章 构建可演进的架构」第 125 段(text/20-ch11.txt:125,搜「接口填充程序」)与第 126 段(text/20-ch11.txt:126,搜「压缩」)。

  13. 出处:「第11章 构建可演进的架构」第 139 段(text/20-ch11.txt:139,搜「隐性知识」)。

  14. 出处:「第11章 构建可演进的架构」第 151 段(text/20-ch11.txt:151,搜「pontoonWorples」)与第 163 段(text/20-ch11.txt:163,搜「pontoonFlubberizedWorples」)。

  15. 出处:「第11章 构建可演进的架构」第 168 段(text/20-ch11.txt:168,搜「隐藏的参数需求」)与第 173 段(text/20-ch11.txt:173,搜「JSON schema」)。

  16. 出处:「第11章 构建可演进的架构」第 187 段(text/20-ch11.txt:187,搜「高内聚和低耦合」)与第 188 段(text/20-ch11.txt:188,搜「爆炸半径」)。

  17. 出处:「第11章 构建可演进的架构」第 192 段(text/20-ch11.txt:192,搜「前端、中间层和后」)与第 196 段(text/20-ch11.txt:196,搜「协调成本」)。

  18. 出处:「第11章 构建可演进的架构」第 200 段(text/20-ch11.txt:200,搜「domain-driven design」)与第 203 段(text/20-ch11.txt:203,搜「最复杂的情况下」)。

  19. 出处:「第11章 构建可演进的架构」第 259 段(text/20-ch11.txt:259,搜「向前兼容的变更」)与第 262 段(text/20-ch11.txt:262,搜「向后兼容的变更则恰恰相反」)。

  20. 出处:「第11章 构建可演进的架构」第 271 段(text/20-ch11.txt:271,搜「SayHello」)与第 332 段(text/20-ch11.txt:332,搜「字段序号」)。

  21. 出处:「第11章 构建可演进的架构」第 331 段(text/20-ch11.txt:331,搜「增加一个新的字段」)与第 344 段(text/20-ch11.txt:344,搜「废弃字」)。

  22. 出处:「第11章 构建可演进的架构」第 307 段(text/20-ch11.txt:307,搜「可怕的」)与第 311 段(text/20-ch11.txt:311,搜「必填项永远存在」)。

  23. 出处:「第11章 构建可演进的架构」第 361 段(text/20-ch11.txt:361,搜「版本化你的 API」)与第 365 段(text/20-ch11.txt:365,搜「API 网关或服务网格」)。

  24. 出处:「第11章 构建可演进的架构」第 90 段(text/20-ch11.txt:90,搜「代价」)与第 388 段(text/20-ch11.txt:388,搜「面向客户的 API 是」)。

  25. 出处:「第11章 构建可演进的架构」第 394 段(text/20-ch11.txt:394,搜「存续的时间更短」)。

  26. 出处:「第11章 构建可演进的架构」第 406 段(text/20-ch11.txt:406,搜「丧失自主性」)与第 412 段(text/20-ch11.txt:412,搜「安全」)。

  27. 出处:「第11章 构建可演进的架构」第 432 段(text/20-ch11.txt:432,搜「读取者和写入者」)与第 435 段(text/20-ch11.txt:435,搜「灵活性和隔离」)。

  28. 出处:「第11章 构建可演进的架构」第 454 段(text/20-ch11.txt:454,搜「无模式并不意味着」)。

  29. 出处:「第11章 构建可演进的架构」第 476 段(text/20-ch11.txt:476,搜「Willa」)与第 465 段(text/20-ch11.txt:465,搜「大杂烩」)。

  30. 出处:「第11章 构建可演进的架构」第 486 段(text/20-ch11.txt:486,搜「CREATE TABLE users」)与第 480 段(text/20-ch11.txt:480,搜「合理性检查」)。

  31. 出处:「第11章 构建可演进的架构」第 502 段(text/20-ch11.txt:502,搜「自取灭亡」)。

  32. 出处:「第11章 构建可演进的架构」第 521 段(text/20-ch11.txt:521,搜「变更操作不那么容」)与第 537 段(text/20-ch11.txt:537,搜「liquibase formatted sql」)。

  33. 出处:「第11章 构建可演进的架构」第 570 段(text/20-ch11.txt:570,搜「DATABASECHANGELOG」)。

  34. 出处:「第11章 构建可演进的架构」第 575 段(text/20-ch11.txt:575,搜「生命周期联系在一起」)。

  35. 出处:「第11章 构建可演进的架构」第 594 段(text/20-ch11.txt:594,搜「不会重新创建那些曾经存储」)与第 595 段(text/20-ch11.txt:595,搜「备份整张表」)。

  36. 出处:「第11章 构建可演进的架构」第 619 段(text/20-ch11.txt:619,搜「extract transformation load」)与第 628 段(text/20-ch11.txt:628,搜「停滞」)。

  37. 出处:「第11章 构建可演进的架构」第 632 段(text/20-ch11.txt:632,搜「change data capture」)与第 635 段(text/20-ch11.txt:635,搜「隐含的 API」)。

  38. 出处:「第11章 构建可演进的架构」第 644 段(text/20-ch11.txt:644,搜「显式解耦的数据产」)与第 648 段(text/20-ch11.txt:648,搜「不必冻结其内部的数据库」)。