[toc]
todo 《A Philosophy of Software Design》
todo 《Software Design X-Rays》
开源项目运营、许可与商业价值 → Software-开源项目成功之道.md
- the three most impactful points are interfaces, stateful systems, and data models.
- Interfaces are contracts between systems. Effective interfaces decouple clients from the encapsulated implementation. Durable interfaces expose all the underlying essential complexity and none of the underlying accidental complexity.
- Delightful interfaces are Eagerly discerning, discerningly eager.
- State is the hardest part of any system to change, and that resistance to change makes stateful systems another critical leverage point. State gets complex faster than other systems and has an inertia that makes it relatively expensive to improve later.
- 安全隐私合规:As you incorporate business obligations around security, privacy, and compliance, changing your stateful systems becomes even more challenging.
- Data models are the intersection of the interfaces and state, constraining your stateful system’s capabilities down to what your application considers legal.
- A good data model is rigid: it only exposes what it genuinely supports and prevents invalid states’ expression.
- 兼容性:A good data model is tolerant of evolution over time.
- Effective data models are not even slightly clever.
- 核心原则:确保实验组 (Experiment) 和对照组 (Control) 在统计学上的同质性 (Homogeneity),唯一变量是实验策略。
- 流量分配:通常基于 Hash(User_ID) % 1000 进行分桶。
- 分流模型:
- 正交分层:不同层级的实验(如 UI 层 vs 算法层)相互正交,流量复用。
- 互斥实验:同一层级的不同策略实验,流量互斥。
- 现象:在分组比较中占优势的一方,在总评中反而处于劣势。
- 现象:只统计了留存下来的用户,忽略了流失用户。
- 场景:长周期实验中,实验组策略导致低活跃用户流失,剩下的高活跃用户拉高了平均指标,看似实验效果正向,实则总量下降。
- If you have a development velocity problem, it might be optimizing test runtimes, moving your Docker compile step onto a RAM disk, or using the techniques described in Software Design X-Rays to find the specific files to improve.
这些原则不是口号,而是降低复杂度、缩小变更半径、提升可验证性的工程约束。对人类工程师如此,对 coding agent 更如此:agent 最容易犯的错不是“不会写代码”,而是过度改动、隐式假设、跳过验证、为了显得聪明而制造不必要结构。
Fail-fast 的核心是:错误一旦出现,应尽早、明确、带上下文地失败,而不是在远处以模糊副作用的形式爆炸。
- 适用场景:参数校验、配置加载、依赖不可用、状态不一致、数据格式不合法、权限或资源缺失。
- 工程价值:缩短 debug 路径,让调用方知道“哪里坏了、为什么坏、需要谁处理”。
- 常见误用:把 fail-fast 理解成“到处抛异常”。真正好的 fail-fast 需要错误信息可行动,并区分用户错误、系统错误、可重试错误和不可恢复错误。
- agent 要求:修改代码前先识别输入边界和失败模式;新增逻辑时优先补清晰校验和可诊断错误;不要吞异常、不要只打印日志后继续运行。
KISS(Keep It Simple, Stupid)的意思不是写“简陋代码”,而是让实现只承载当前问题的必要复杂度。
- 适用场景:新功能、bug fix、临时实验、代码重构、agent 自动生成代码。
- 工程价值:降低理解成本、测试成本和回滚成本。
- 常见误用:为了“简单”牺牲正确性,或者把必要的抽象全部摊平成重复逻辑。
- agent 要求:优先复用现有模式;先做最小正确实现;只有当重复或复杂度真实出现时再抽象;不要新增框架、全局状态、复杂配置或宽泛 helper 来解决局部问题。
DRY(Don’t Repeat Yourself)真正反对的是“同一份知识散落多处”,而不是机械地消灭所有长得像的代码。
- 适用场景:业务规则、字段含义、权限判断、序列化协议、状态流转、公共算法。
- 工程价值:同一规则只需改一处,避免行为漂移。
- 常见误用:过早抽象,把只是表面相似、变化原因不同的逻辑强行合并,最后得到一个参数爆炸的“万能函数”。
- agent 要求:先判断重复的是“知识”还是“形状”。如果只是两段代码长得像,但业务语义和演化方向不同,可以暂不合并;如果重复的是协议、规则或状态机,必须收敛到单一来源。
YAGNI(You Aren’t Gonna Need It)的核心是拒绝为想象中的未来需求付当下复杂度成本。
- 适用场景:扩展点、配置项、策略接口、抽象层、缓存、异步化、多租户、多后端。
- 工程价值:避免系统在真实需求到来前就被“可能有用”的结构绑架。
- 常见误用:用 YAGNI 拒绝必要的边界设计。不会立刻实现未来功能,不等于可以忽略兼容性、数据模型演进和错误边界。
- agent 要求:不要因为“以后可能需要”新增未被当前任务使用的代码、参数、文件、测试或文档;如果确实留下扩展点,要写明当前调用方和立即收益。
单一职责原则强调一个模块应该只有一个主要变化原因。关注点分离强调不同层次的问题不要混在一起。
- 适用场景:业务逻辑与 IO、策略与执行、解析与校验、状态更新与展示、数据访问与领域规则。
- 工程价值:让修改能被局部理解、局部测试、局部回滚。
- 常见误用:把职责单一变成“每三行代码一个函数”,导致调用链碎片化。
- agent 要求:新增代码前先找现有边界;不要把 unrelated concerns 塞进已有函数;拆分时以“变化原因”和“测试边界”为准,而不是按行数机械拆分。
代码行为应符合调用方和维护者的合理预期。命名、默认值、错误处理、返回值语义都应减少意外。
- 适用场景:API 设计、配置默认值、CLI 参数、函数命名、状态迁移、feature flag。
- 工程价值:降低误用概率,减少隐形线上事故。
- 常见误用:过度追求“显得高级”的命名或控制流,让简单行为变得难猜。
- agent 要求:遵循仓库既有命名、目录、错误处理和测试风格;不要引入和周围代码不一致的默认行为;如果必须改变语义,要同步文档和测试。
高质量改动通常有清晰边界、较小 diff、可单独验证,失败时能快速回滚。
- 适用场景:线上系统、基础设施、共享库、数据迁移、agent 长程任务。
- 工程价值:降低 review 难度和事故半径。
- 常见误用:把一个原子变更拆得过碎,导致中间状态不可运行。
- agent 要求:一次只解决一个明确问题;避免顺手重构;如果必须大改,先拆出机械改动、行为改动、验证改动;每一步都能解释“为什么现在必须改”。
好的模型不只是处理错误状态,而是尽量不允许错误状态被构造出来。
- 适用场景:类型设计、枚举、状态机、配置 schema、数据库约束、任务生命周期。
- 工程价值:把运行时错误前移到编译期、构造期或校验期。
- 常见误用:为了追求类型完美而引入过重模型,使简单业务难以演进。
- agent 要求:涉及状态流转时,先列合法状态和转移;优先用 enum / dataclass / schema / invariant 表达约束,而不是靠散落的 if 判断兜底。
- Rust 配置解析示例:enum 按 variant 分流、类型保证校验完成(见 Rust.md)。
工程质量不是“代码看起来对”,而是能被测试、日志、指标和文档持续证明。
- 测试:覆盖核心行为、边界条件、回归 case;不要为了覆盖率给无分支 glue code 写脆弱测试。
- 可观测性:关键路径要能回答发生了什么、耗时多少、失败原因是什么、影响范围多大。
- 文档:记录非显然决策、接口契约、迁移步骤和运维假设;不要解释每一行显而易见的代码。
- agent 要求:改代码后必须尽力运行最相关验证;跑不了要说明原因和替代检查;新增复杂逻辑时同步测试或最小可复现验证。
可以把下面这段作为 coding agent 的任务前置 prompt 或 code review checklist;独立 snippet 见 agent-engineering-quality-prompt.md。
在本次工程任务中,请优先遵守以下软件工程原则:
1. 先理解目标和现有边界,再修改代码。优先复用仓库已有模式、工具函数、测试风格和错误处理方式。
2. Fail-fast:对非法输入、缺失配置、状态不一致和不可恢复错误,尽早给出清晰、可行动的失败信息;不要吞异常或静默降级。
3. KISS:做最小正确改动。不要为了局部任务新增框架、复杂抽象、全局状态或未被使用的扩展点。
4. DRY:消除重复的业务规则、协议和状态知识;但不要把只是表面相似、变化原因不同的代码强行抽象到一起。
5. YAGNI:不要实现当前任务没有用到的未来功能、参数、配置或测试。保留扩展点时必须说明立即收益。
6. SRP / 关注点分离:业务逻辑、IO、解析、校验、状态更新和展示尽量保持边界清晰;拆分以变化原因和可测试性为准。
7. 最小惊讶:命名、默认值、返回值、错误语义和目录位置要符合现有代码习惯。改变行为时同步测试和文档。
8. 小步可回滚:避免顺手重构和无关格式化。若任务较大,拆成机械改动、行为改动和验证改动。
9. 让非法状态不可表达:涉及生命周期、状态机、schema 或配置时,显式列出合法状态和转移,优先用类型或 schema 固化约束。
10. 验证闭环:改完后运行最相关测试、lint、类型检查或最小复现;无法运行时说明原因、风险和替代验证。
输出时请说明:改了什么、为什么这样改、遵守了哪些原则、如何验证、剩余风险是什么。
测试提供的是针对已表达条件的反例搜索与回归证据,不能证明软件没有 Bug。QA(Quality Assurance)比测试更宽:它还包括规格评审、流程设计、质量门禁、风险管理、发布验证、生产监控和复盘。
| 层次 | 主要回答 | 特点与边界 |
|---|---|---|
| 静态检查 | 代码是否违反类型、语法、风格、安全或架构规则 | 不运行程序;反馈快,但不能证明运行时行为 |
| 单元测试 | 一个函数、类或小组件的局部行为是否正确 | 数量多、速度快、失败易定位;通常隔离数据库、网络等外部依赖 |
| 集成测试 | 组件与数据库、文件、队列、外部 API 能否正确协作 | 能发现序列化、事务、配置和协议错误;比单测慢且更依赖环境 |
| Contract test | 服务消费者与提供者是否仍满足约定的请求 / 响应契约 | 比全链路测试轻;不证明完整业务流程正确 |
| 验收测试 | 系统是否满足用户可见的业务规格 | 从外部行为出发,可作为交付 gate;质量取决于规格是否完整 |
| End-to-End | 部署后的完整系统能否走通关键用户旅程 | 置信度高,但慢、贵、易 flaky,只保留少量关键路径 |
| 探索性 / 人工测试 | 是否存在规格没有提前想到的问题 | 擅长发现 usability、异常组合和 unknown unknowns;难以稳定回归 |
测试金字塔是一条反馈成本原则:保留大量小而快的测试、适量边界测试、少量全链路测试。具体形状取决于系统,不能把“单测数量最多”机械化成目标。Practical Test Pyramid
Gherkin 是 Cucumber 使用的结构化自然语言 DSL,不是测试执行引擎。它用 Given / When / Then 描述初始状态、行为和可观察结果,再由 step definition 映射到测试代码:
Feature: 支付幂等
Scenario: 相同幂等键重复提交
Given 订单尚未支付
When 客户端使用相同幂等键提交两次支付
Then 只产生一笔扣款
And 两次请求返回同一个支付结果好的 Gherkin 面向领域行为,不写 CSS selector、内部函数或数据库实现;Then 检查用户或外部系统能观察到的结果。它的价值是让产品、QA 和工程师共同审查“系统应该做什么”,并留下机器可执行的 acceptance criteria。Gherkin 文件能运行,不代表规格完整;遗漏的场景依然不会被测试发现。
- Line / statement coverage:测试执行过哪些语句。
- Branch coverage:条件的不同分支是否都被走过。
- Condition coverage:复合条件中的各个布尔项是否取过不同结果。
Coverage 只证明代码被执行过,不证明断言能识别错误。一个没有有效 assertion 的测试也可以得到很高覆盖率。因此:
- 低覆盖率是强烈的风险信号,说明存在自动化测试从未触达的区域。
- 高覆盖率只是弱正向信号,不能替代测试设计、边界 case 和业务验收。
- 不存在适用于所有项目的统一目标值;应按业务风险、复杂度、变更频率和寿命制定门槛。
- Coverage 更适合用来寻找测试空白,而不是作为让团队刷到某个百分比的 KPI。Google Code Coverage Best Practices
变异测试会自动对生产代码注入小错误,例如把 > 改成 >=、删除一次调用、替换返回值,然后重新运行测试:
baseline tests pass
-> generate mutant
-> run relevant tests
-> test fails: mutant killed
-> test passes: mutant survived
Coverage 问“测试是否执行过这段代码”;mutation testing 问“这段代码被改错后,测试是否会察觉”。常用指标是 mutation score = killed mutants / valid mutants。survived mutant 通常表示断言或 case 太弱,但也可能是行为等价、无业务影响的 equivalent mutant,需要人工判读。PIT 基础概念
Mutation testing 的成本较高,因为每个 mutant 都可能触发一次测试。生产实践通常只变异本次改动、按历史有效性筛选 operator,并只运行相关测试;Google 的增量方案也是把它放进 code review,而不是每次扫描整个仓库。Practical Mutation Testing at Scale
- Property-based testing:给出 invariant,由框架生成大量输入并缩减失败样例。例如对任意列表,
sort(sort(xs)) == sort(xs)。它扩大输入空间,但仍依赖人先写对 property。 - Fuzzing:持续生成畸形、随机或 coverage-guided 输入,主要寻找 crash、越界、hang 和安全缺陷。OSS-Fuzz
- Regression testing:把曾经出现的 Bug 固化为测试,防止相同行为再次出现。
- Performance / load / soak testing:分别检查延迟与吞吐、并发负载、长时间运行下的泄漏和退化。
- Security testing:结合 SAST、依赖扫描、DAST、fuzzing、权限与威胁模型;普通功能测试无法覆盖其全部风险。
一条可执行的质量链路通常是:
规格与风险评审
-> static checks + unit tests
-> integration / contract tests
-> acceptance / critical E2E
-> security / performance gates
-> risk-based review
-> canary / feature flag
-> production metrics + rollback
质量指标也应覆盖多个维度:
| 维度 | 可用信号 |
|---|---|
| 测试有效性 | coverage gap、mutation score、flaky rate、测试耗时 |
| 功能质量 | escaped defects、回归缺陷、验收通过率 |
| 可维护性 | 复杂度、重复、依赖环、hotspot、变更耦合 |
| 可靠性与性能 | error rate、p95 / p99 latency、资源水位、SLO |
| 交付风险 | change failure rate、rollback rate、MTTR |
| 安全 | 高危依赖、静态 / 动态扫描结果、权限越界与漏洞修复时长 |
指标是 proxy,不是目标本身。Coverage 可以靠无效测试刷高,复杂度可以靠机械拆函数降低,测试通过率也会因跳过 flaky case 变好。质量 gate 必须和真实风险、生产反馈及人工判断交叉验证。
- 新增回归测试应先在旧实现上失败,再在修复后通过,证明它有辨别力。
- expected result 应来自规格或独立 oracle,不能照抄被测实现的输出。
- 除 happy path 外,覆盖边界值、非法输入、部分失败、重试、幂等、并发与状态恢复。
- Mock 外部边界,不要把核心业务逻辑全部 mock 掉;关键集成仍需面对真实或高保真依赖。
- 优先审查 assertion 和测试意图,而不是只看测试数量与 coverage 增量。
Building Evolutionary Architectures and Reclaim unreasonable software.
- What percentage of the code is statically typed?
- How many files have associated tests?
- What is test coverage within your codebase?
- How narrow are the public interfaces across modules?
- What percentage of files use the preferred HTTP library?
- Do endpoints respond to requests within 500ms after a cold start?
- How many functions have dangerous read-after-write behavior? Or perform unnecessary reads against the primary database instance?
- How many endpoints perform all state mutation within a single transaction?
- How many functions acquire low-granularity locks?
- How many hot files exist which are changed in more than half of pull requests?
- the number of files changed in each pull request on the understanding
- smaller pull requests are generally higher quality.
- measure a codebase’s lines of code per file
- on the assumption that very large files are generally hard to extend.
- instrumentation is a requirement for useful metrics. Instrumentation complexity is the biggest friction point for adopting these techniques in practice, but if you can push through, you unlock something pretty phenomenal: a real, dynamic quality score that you can track over time and use to create a clarity of alignment in your approach that conceptual alignment cannot.
-
Intro
- maybe one engineer working on developer tooling for every fifteen product engineers, in addition to your infrastructure engineering investment.
-
人员配置:
- Technical Program Manager, but typically that is after they cross into operating a Quality program
- 1-N个P9兼管
-
要点:
- Trust metrics over intuition.
- Keep your intuition fresh
- team embedding、team rotation、1:1 discussion
- Listen to and learn from your users.
- Do fewer things, but do them better
- Don’t hoard impact.
-
衡量产出:
- discounted developer productivity (in the spirit of discounted cash flow)
参考:W. W. Royce, Managing the Development of Large Software Systems, 1970;Agile Manifesto、Agile Principles。
瀑布式开发把软件项目拆成线性阶段:需求、规格、设计、实现、集成、测试、交付 / 运维。每一阶段有明确产物和 sign-off,下游依赖上游完成,像水从上游流到下游。
它的设计动机不是“慢”,而是用阶段门管理承诺:先把需求、预算、责任、文档、验收口径和合同边界固定下来,再进入实现。它适合需求稳定、变更成本高、合规文档重、硬件 / 外包 / 多团队依赖强的项目。
核心问题在于,软件开发往往不是制造业复制,而是知识发现。瀑布隐含三个强假设:
- 需求能在早期说清。
- 设计能在实现前接近正确。
- 集成和测试可以后置。
一旦这些假设不成立,错误会沿阶段向下游滚动:需求误解到测试阶段才暴露,设计缺陷到集成阶段才发现,返工成本就会非常高。瀑布最危险的地方不是文档多,而是反馈太晚。
更好的理解:
- 瀑布适合管理外部承诺:合同、审计、里程碑、供应商、合规验收。
- 敏捷 / 迭代适合管理不确定性:用户需求、产品体验、技术方案、模型行为、真实数据反馈。
- 真实组织里通常是混合形态:外层有阶段门,内层用短迭代交付可运行软件。
一句话:瀑布式开发的本质是用计划和阶段门降低管理不确定性;敏捷的本质是用更早、更频繁的工作软件和用户反馈降低产品 / 技术不确定性。关键不在流程标签,而在反馈是否早于不可逆承诺。
前后端联调指前端页面与后端服务在开发阶段进行接口数据对接、调试与验证的过程。核心是“契约先行”:先约定 URL、方法、参数、返回结构,再并行开发,联调阶段验证一致性。
- 接口契约(API contract)
- 先定义再开发:用 Swagger / OpenAPI、YApi、Apifox 等工具沉淀文档;前端可先用 Mock 数据开发,后端完成后切真实接口
- 变更管理:字段变更尽量收敛到适配层,不直接散落到业务组件;联调期用 Network 面板 / Postman / 抓包工具核对请求与响应
- 常见联调问题:参数名/类型不一致、返回结构漂移、跨域(CORS)、鉴权/token、空值语义、重复请求、环境根地址配置
- TOP 接口 / top 接口
- 大写 TOP:Taobao Open Platform(淘宝开放平台)的缩写,指淘宝/阿里系对外开放数据和能力的 HTTP API;外部服务、小程序、千牛插件都会调用
- 调用特点:REST 风格,大部分接口支持 GET/POST,写操作只支持 POST;用 app key + 签名请求 TOP 服务器,返回业务数据
- 前端调用常见入口:千牛插件用
QN.top.invoke()/QN.top.batch(),小程序用cloud.topApi.invoke();涉及权限/敏感数据的接口一般要在服务端转发 - 联调语境里若看到小写
top,多半不是淘宝 TOP:可能是window.top(iframe 嵌套时返回最顶层窗口,常配合 postMessage 做跨层通信),也可能是团队内部对“顶层聚合接口/BFF 入口”的简称;先看上下文再判断
一套典型 SaaS 研发链路的串联视角:代码托管(GitHub / GitLab)→ 质量门禁(SonarCloud)→ 凭据管理(1Password)→ 团队文档(Confluence)→ 任务跟踪(JIRA)→ 前端部署(Netlify)。
GitHub
- 定位:全球最大代码托管与协作平台(git 仓库 + PR / issue + 社交化开源)。
- 核心:PR review 流程、GitHub Actions(CI/CD)、Codespaces、Packages、Copilot / AI 助手、开源生态(stars / forks / discussion)。
- 适合:开源项目与默认云端托管;生态最全但平台有绑定。
GitLab
- 定位:DevOps 一体化平台,一个应用内包含 repo + CI/CD + 安全扫描 + 容器 / 部署 + wiki。
- 核心:Single Application 理念;支持自托管(CE / EE),数据不出内网;内置 CI/CD(
.gitlab-ci.yml)、代码质量、SAST / 依赖扫描。 - 对比 GitHub:GitHub 偏「生态 + 协作」,GitLab 偏「一体化 + 可自托管」,适合数据合规 / 私有化要求高的团队。
SonarCloud
- 定位:代码质量与安全静态分析云服务(SonarQube 的 SaaS 版)。
- 核心:扫描 Bug、漏洞、坏味道、重复代码、测试覆盖;质量门禁(Quality Gate)决定能否合入;支持主流语言并与 GitHub / GitLab CI 集成。
- 对比:GitHub 自带 CodeQL / secret scanning 偏安全;Sonar 偏「可维护性 + 质量门禁」;本地 lint 只管语法风格,Sonar 管跨文件与历史趋势。
1Password
- 定位:团队密码与密钥管理(password manager)。
- 核心:密码 / 密钥 / SSH key 集中存储,按团队保险库(Vault)共享,浏览器 + CLI 集成,2FA,开发者工具(
opCLI、CI secret 注入)。 - 边界:只管理「凭据」,不替代
.env或云 Secret Manager——本地开发用.env(不入库),生产 / CI 用平台 Secret Manager(Vault / AWS Secrets Manager),1Password 偏「人与开发者的凭据」。已有提及见 Security-Privacy-Cryptography.md「密码管理器」。
Confluence
- 定位:团队知识库 / 协作文档(Atlassian)。
- 核心:空间(Space)+ 页面层级 + 权限 + 模板(技术方案、周报、API 文档)+ 与 JIRA 双向引用。
- 对比:Notion 更灵活 / 个人向;Confluence 适合按团队空间做权限与审计。文档原则:可检索、有 owner、定期清理。
JIRA
- 定位:项目与问题跟踪(Atlassian),敏捷开发事实标准之一。
- 核心:issue / 史诗 / 故事 / 任务 + 看板 / 冲刺 + 自定义工作流 + 与 Git 集成(提交 → issue 关联 → PR 自动关闭)。
- 对比:GitHub Issues 轻量够用(开源 / 小团队);JIRA 强在规模化流程与报表;Linear 体验更现代但生态浅。
Netlify
- 定位:前端 / 静态站点托管与部署平台(Jamstack 代表)。
- 核心:Git 推送即部署、PR 预览部署(Preview Deployments)、CDN + 边缘、Serverless Functions、表单 / 身份、回滚。
- 对比:Vercel 同为前端部署平台(更偏 Next.js 生态);Netlify 偏静态站 / 内容站点;个人轻量站也可用 GitHub Pages。已提及见 Web-基础.md「部署到 Vercel、Netlify、Cloudflare 这类平台」。
选型一句话:开源 / 云原生协作选 GitHub;私有化 / 合规一体化选 GitLab;质量门禁接 SonarCloud;人与开发者凭据用 1Password;规模化文档与任务用 Confluence + JIRA;前端发布用 Netlify / Vercel。
来源:各官方站点 GitHub、GitLab、SonarCloud、1Password、Confluence、JIRA、Netlify。
todo 《Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations》
- DevOps的重点:
- version control
- trunk-based development
- CI/CD
- production observability (including developers on-call for the systems they write)
- working in small, atomic changes.
经验中,云原生系统的可观测性开销,往往占到云开销的 15%-25%。
这么高吗?
- 阿里云有非常丰富的可观测性服务,包括日志服务 SLS,云监控 CloudMonitor, 应用实时监控服务 ARMS
- Grafana:SpaceX 的数据监测利器,云原生领域的 Tableau
- 本质是提升数据可观测性(Data Observability),打破数据边界,提供一个“统一的视窗”,实现对数据的全览和实时监控
- 也有观点认为,可视化的重要性远大于指标、日志和链路追踪
- 推动“数据民主化”
系统迁移是在公司和代码库增长过程中,唯一能够规模化解决技术债的有效机制。当公司快速发展时,任何工具或流程都将达到其规模上限,迁移因此成为必然。有效的迁移能力是维持组织高效迭代的关键,否则最终将陷入技术债的泥潭或被迫进行更具破坏性的完全重写。
一次成功的迁移可以遵循一个标准化的三阶段手册:
- 去风险 (Derisk)
- 目标: 尽快、低成本地验证方案并建立信任。
- 执行:
- 与最困难、最边缘的团队深入沟通,迭代设计文档。
- 不要从最简单的案例开始。选择并嵌入1-2个最复杂的团队,与他们共同构建并完成迁移,这能真正暴露方案的弱点。
- 成功完成早期迁移是为后续大规模推广建立信誉的关键。
- 赋能 (Enable)
- 目标: 规模化推广,降低整个组织的迁移成本。
- 执行:
- 构建自动化工具: 投入时间开发能自动化处理90%简单场景的迁移工具,而不是急于分发任务。
- 为剩下10%的复杂场景提供清晰的文档和支持。
- 完成 (Finish)
- 目标: 彻底终结项目,不留尾巴。
- 执行:
- 设定明确的截止日期: 这是确保项目完成的最有效手段。
- 停止支持旧系统: 在截止日期后,正式停止对旧系统的维护,推动剩余部分完成迁移。
- 清理旧代码: 迁移完成后,务必将旧代码和基础设施彻底移除。
来源:Martin Fowler - Strangler Fig Application、TypeScript Project References。
模式:Strangler Fig(绞杀榕)是 Martin Fowler 提出的应用现代化模式。名字来自绞杀榕:种子落在宿主树冠上,根系沿树干向下包裹,最终宿主枯死、榕树独存。对应到软件:不一次性重写旧系统,而是用新系统在旧系统旁边/前面逐步绞杀它,直到旧系统不再被调用后拆除。它专门反对 Big Bang rewrite——重写周期长、期间零交付、需求漂移后回不来。
“逐步替换”怎么执行:
- 旧系统继续运行,入口加一层 facade / router 拦截流量;
- 按模块或功能切片,把一部分请求路由到新实现,新老并行;
- 验证一块、替换一块,通过 feature flag / 比例放量逐步扩大新系统接管范围;
- 旧系统只剩“还在被调用”的最后几块时集中替换;
- 确认零调用后彻底拆除旧系统(对应迁移三阶段的 Finish)。
为什么有效:每步都有交付、可回滚、风险局部化;替换顺序按边界清晰度而非架构美观排序;旧系统里稳定的部分可以留到最后甚至保留。
关键条件与坑:
- 接口兼容与数据一致性:新老实现共享同一份数据或做好迁移同步,否则分流后行为分裂;
- 从 bounded context 边界清晰处开切:边界糊的地方先划边界,再谈替换;
- 可观测性必须能回答“旧系统是否还在被调用”,否则不知道何时收尾;
- 收尾必须拆旧代码,否则变成“新系统 + 僵尸旧系统”双倍维护。
工程约束变体(LoopX / TypeScript):Strangler Fig 只规定替换顺序,不规定“谁在改”。LoopX 在此基础上加硬约束:每个 revision、每个语义块只有一个 owner——同一语义块同时只允许一个 agent / 进程拥有写入权,从控制面杜绝并发改写同一块导致状态漂移。TypeScript 的 project references 则把 bounded context 变成编译期边界:composite project + 显式 references + 构建顺序,让“哪块依赖哪块、谁可以引用谁”由类型系统把关,逐块替换在编译层面可控。机制详见 AI-Agent-Engineering.md - LoopX。
来源:H. T. Kung and John T. Robinson, On Optimistic Methods for Concurrency Control, ACM TODS 1981。
OCC 的核心不是“不处理冲突”,而是 先不加锁地并发做,提交前做 validation:验证通过才把本地修改原子写回全局状态;验证失败就 abort / retry。
read phase:
读全局状态;写操作只写本地 copy
validation phase:
检查这次 transaction 是否可串行化
write phase:
validation 通过后,把本地 copy 原子写回全局
它适合冲突概率不高、读多写少、希望避免长时间持锁的系统。代价是:冲突会在提交前才暴露,失败事务需要重试;如果冲突率高,OCC 会把成本从“等待锁”转成“反复 abort / retry”。
并发事务的最终结果,必须等价于某个串行执行顺序。形式上,如果初始数据库状态为 d_initial,事务集合为 T_1 ... T_n,那么并发执行后的结果应等价于某个排列 π 的串行组合:
T_{\pi(n)} \circ \cdots \circ T_{\pi(1)}(d_{\text{initial}}) $$
这个目标比“每个事务自己看起来没错”更强。并发系统真正要保证的是:虽然实际执行交错发生,但外部观察到的状态变化像是事务按某个顺序一个个完成。
对一个准备提交的新事务 T_j,validation 要检查所有在串行顺序上更早的事务 T_i。直觉是:更早事务的写入,不能破坏 T_j 已经读到的东西,也不能和 T_j 即将写入的东西产生不可串行化冲突。
记:
R(T) = transaction T 的 read set
W(T) = transaction T 的 write set
典型安全条件可以这样理解:
T_i完全早于T_j:T_i写完后,T_j才开始读。这等价于普通串行顺序,安全。T_i与T_j读阶段重叠,但T_i写入的内容没有被T_j读过:
这表示 T_j 没有基于被 T_i 改写过的旧值做决策,因此可以把 T_i 排在 T_j 前面。
- 更强的安全条件是:
T_i写入的内容既不影响T_j读到的东西,也不和T_j即将写的东西相交:
这说明两者虽然时间上重叠,但在数据依赖上互不干扰,可以安全并发。
| 机制 | 核心思路 | 适合场景 |
|---|---|---|
| Pessimistic locking | 先加锁,再读写,提前阻止冲突 | 冲突率高、写入代价大、不能接受重试 |
| OCC | 先并发执行,提交前验证,不通过就 retry | 冲突率低、读多写少、希望减少锁等待 |
| WAL | 正式数据持久化前,先持久化足以恢复的记录 | 崩溃恢复、事务 durability、延迟刷写数据页 |
| Event Sourcing | 把状态变化记录成事件流,用 replay / projection 重建状态 | 需要审计、回放、历史状态、并行 read model |
| CRDT | 让并发更新天然可合并,减少中心化冲突检测 | 分布式、离线、多副本协作编辑 |
OCC 解决的是 提交时能不能接受这次写入;WAL 解决的是 已接受的写入如何经受进程或机器崩溃;Event Sourcing 解决的是 业务状态变化如何被记录、重放和审计;CRDT 解决的是 多个副本并发更新如何自动收敛。它们不是互斥关系:数据库可以用 OCC 做提交验证,用 WAL 保证持久化;应用再用 domain event / event log 记录已提交的业务事实,并用 projection 服务读路径。
应用场景:versioned agent memory 提交协议。
Agent memory / experience 系统里也有类似事务问题:多个 session、heartbeat、goal tick 或 meta-agent 可能同时读旧 memory,然后生成 patch。不能因为大家都“想改进 memory”,就直接 append / overwrite。
可以把一次 memory update 看成事务:
read set:
当前任务读过的 memory ids / data_version / policy view
local write:
生成的 memory patch、merge proposal、delete / bury decision
validation:
检查 read set 是否仍是当前版本;检查 write set 是否和已提交 patch 冲突
write:
apply patch,生成新的 data_version,并写入 event log
最小字段可以这样补到 versioned memory / eval 系统里:
memory_patch_txn:
txn_id
source_run_id
read_data_version
read_set
write_set
generated_patch
validation_status: accepted | aborted | retry_required | manual_merge_required
committed_data_version
这能避免两类常见问题:
- lost update:两个 agent 基于同一个旧版本生成 patch,后提交的覆盖先提交的。
- future leakage:eval 时把某个 run 当时不可见的后续 memory patch 也算进 policy view。
因此 OCC 和 Event Sourcing 是互补的:OCC 让 memory patch 在提交前验证依赖是否仍成立;Event Sourcing / versioning 让提交后的状态变化可追踪、可回放、可按 data_version 解释历史行为。
来源:ARIES、PostgreSQL WAL、SQLite WAL、RocksDB WAL format、Linux ext4 journal、Raft paper。
WAL 的核心是一条持久化顺序约束:
在正式数据页、索引、内存表对应的持久化状态落盘前,先把足以恢复该修改的日志记录刷到稳定存储。
数据库可以先修改内存中的 buffer page;真正不能发生的是:脏数据页已经持久化,而描述这次修改的 WAL 还没有持久化。
修改路径:
修改内存中的 buffer page
-> append WAL record
data page 写回门槛:
flush WAL through page LSN
-> 才允许对应 dirty page 写回正式数据文件
transaction 提交门槛:
append commit record
-> flush WAL through commit LSN
-> 才向调用方确认 committed
两个关键不变量:
写回 data page 前:
durable_wal_lsn >= page_lsn
确认 transaction committed 前:
durable_wal_lsn >= commit_lsn
其中 LSN(Log Sequence Number)是日志记录的单调位置。它让系统知道数据页已经包含到哪条日志、恢复应从哪里继续。
如果每次事务提交都随机写回所有数据页,I/O 成本很高。WAL 把同步路径收敛成顺序追加:
commit path:
sequential append + fsync(WAL)
background path:
batch flush dirty pages
checkpoint
recycle old WAL
顺序写通常比散落的数据页随机写便宜;多个并发事务还可以通过 group commit 共用一次 WAL flush。PostgreSQL 因此不要求每次提交都同步刷完所有被修改的数据页。
但 write() / append 返回成功不等于已经耐久。数据可能仍在操作系统 page cache、磁盘控制器缓存或设备易失缓存中。真正的 durability 取决于:
fsync/fdatasync或等价持久化屏障;- WAL 与数据文件之间的 flush ordering;
- commit record 何时被认为稳定;
- checksum、record length 等 torn-write / partial-record 检测;
- 存储设备是否诚实实现 flush。
一条通用 WAL record 常包含:
lsn
transaction_id
record_type
target_page / key
redo information
optional undo information
previous_lsn
length + checksum
崩溃后,系统从 checkpoint 附近扫描 WAL:
读取 checkpoint
-> 丢弃尾部不完整或 checksum 错误的 record
-> REDO:重做已持久化日志、但尚未进入正式数据页的修改
-> 可选 UNDO:撤销崩溃时未提交事务已经写出的修改
-> 重新建立一致状态
并非所有 WAL 都同时支持 REDO 和 UNDO:
- PostgreSQL、RocksDB 等常见路径主要依赖 redo;
- ARIES 这类 undo/redo recovery 会分析事务、重复历史,再撤销 loser transactions;
- 日志只保存 after-image、before-image、physical page delta 还是 logical operation,决定了能执行哪种恢复。
checkpoint 不是“日志已经没用”。它只建立一个更近的恢复起点。只有当相关状态已安全进入正式存储、没有 reader / replica / backup 再依赖旧日志时,旧 WAL 才能回收。
| 场景 | WAL 记录什么 | 恢复方式与边界 |
|---|---|---|
| 关系数据库 | page change、transaction 与 commit record | 从 checkpoint redo;具体系统可能还需要 undo / MVCC cleanup |
| SQLite | 修改先追加到 -wal 文件,主数据库保持旧版本 |
reader 固定自己的 end mark;checkpoint 把 WAL page 合并回主文件;仍只有一个 writer |
| RocksDB / LSM KV | WriteBatch 先进入 WAL,再更新 MemTable |
crash 后 replay WAL 重建尚未 flush 成 SSTable 的 MemTable |
| ext4 / journaling filesystem | metadata 或 data block transaction + commit block | 没有合法 commit/checksum 的事务在 replay 时丢弃;完成事务再写回 home location |
| 2PC participant / coordinator | prepare state、commit / abort decision | 节点重启后恢复 in-doubt transaction;WAL 不会消除等待 coordinator 的阻塞问题 |
| Raft replicated state machine | term、vote 与 command log 持久化,并复制到 quorum | committed entry 才 apply 到 state machine;这是“本地 WAL + 分布式共识”,不能只靠 append 本地文件替代 |
| durable job / workflow | task transition、input、attempt、result intent | 启动时 replay 到状态机;外部副作用还要靠 idempotency key、receipt 或补偿协议 |
最后两类需要注意:
- Raft log 不只是 WAL。WAL 解决单节点崩溃恢复;Raft 还要建立跨副本的一致顺序和 commit quorum。
- 应用状态机可以使用 WAL 思路,但不宜轻易手写存储引擎。多数业务先使用数据库事务、SQLite 或成熟 KV,再把业务状态机建在其上。
| 概念 | 核心目的 | 是否通常是业务 source of truth |
|---|---|---|
| WAL | 存储层 crash recovery 与 durability | 否;可 checkpoint、归档或回收 |
| Event Sourcing | 用 domain event 定义和重建业务状态 | 是 |
| Transactional Outbox | 将“业务提交”和“待发送消息”放进同一数据库事务 | Outbox row 是可靠投递意图,不是底层 WAL |
| Consensus log | 在多个副本间建立一致的 command 顺序 | 是 replicated state machine 的提交依据 |
| Observability log | 调试、搜索、监控 | 通常不是 correctness 依赖 |
一个订单系统可能同时拥有:
PostgreSQL WAL
保证订单表和 outbox 表的事务持久化
Outbox
保证 OrderPaid 消息最终交给 broker
Domain event
表达“订单已支付”这个业务事实
Application log
记录 handler 延迟和错误,供排障
四者都可能是 append-only,却承担不同正确性责任。
- WAL 不是备份。 磁盘损坏、误删除或错误操作可能同时影响数据与日志;备份需要独立副本和恢复演练。
- WAL 不是自动幂等。 recovery 可能重复执行 record,必须用 page LSN、transaction state、sequence 或幂等操作避免二次生效。
- WAL 不能安全重放任意外部副作用。 发邮件、扣款、调用外部 API 需要 intent/outcome、idempotency key、receipt 或补偿;不能把日志 replay 直接等同于再次执行。
- checkpoint 太少会让日志膨胀、恢复变慢;太频繁会增加写放大与延迟。
- 关闭同步刷盘是在改变 durability contract。 吞吐提升来自允许掉电后丢失最近提交,不能仍对外宣称严格 durable。
一句话:
WAL 是“先留下足以恢复的证据,再允许正式状态落盘”的存储协议;Event Sourcing 是业务状态模型,Outbox 是跨系统投递协议,Raft 是复制与共识协议。
来源:Martin Fowler: Event Sourcing、OpenViking discussion #2277: Memory Data Versioning。
Event Sourcing 的核心是:系统状态不是直接被覆盖保存,而是由一串事件推导出来。
event log
-> replay / projection
-> current state
也就是说,系统不只保存“订单现在是什么状态”,而是记录:
OrderSubmitted
PriceChanged
PaymentCaptured
OrderShipped
再由 handler / projector 把事件流投影成当前状态。latest state 可以存成 cache / snapshot,但它不是唯一真相;真正可重建、可审计、可回放的是 event log。
| 能力 | 含义 |
|---|---|
| Complete rebuild | 从 event log 重新构建当前状态,修复 projection bug 或迁移新模型 |
| Temporal query | 查询某个历史时刻的状态,而不是只看 latest |
| Event replay | 用旧事件流驱动新 handler / projection,验证新逻辑 |
| Parallel model | 同一事件流可以投影出多个 read model / index / report |
| Audit / debugging | 状态如何一步步变成现在这样,有可追踪依据 |
这和普通“记录日志”不一样:日志经常只是观测副产物;Event Sourcing 里的 event 是系统状态变化的源事实。状态表、索引、报表、缓存只是 projection。
OpenViking 的 memory versioning 设计可以看作 Event Sourcing 的一个工程化变体:memory 文件正文保存最新版本,历史状态通过文件内 VERSION_HISTORY 的 reverse diff 链回退得到。
latest memory file
+ VERSION_HISTORY(reverse diffs)
-> materialize_memory_at_version(data_version)
-> historical memory state
核心动机不是“想看历史”这么简单,而是让 memory / experience 系统具备按时间回到当时可见状态的能力:
Task A consumes policy view(v1)
Task A finishes -> MemoryPatchApplied(E1) -> data_version=v2
Task B consumes policy view(v2)
如果事后只看 latest memory(v2),就会把 Task A 解释成“明明知道 E1 还做错了”。但 Task A 当时的 policy view 其实是 v1,E1 尚未存在或尚未更新。版本化 memory 的价值,是让分析时能明确区分:
generated_at_version
applied_at_version
consumed_at_version
evaluated_at_version
这类字段能避免未来知识污染历史归因。
OpenViking discussion #2277 采用一个低成本一期方案:
search(query, data_version=X)
-> 用最新向量索引召回候选文件
-> 对每个候选文件 materialize 到 <= X 的最近版本
-> 过滤当时不存在或当时已删除的文件
-> 返回目标版本视角下的内容
这个设计的优点是不用为每个历史版本维护独立 embedding,存储和索引成本低;缺点也很明确:历史检索不是严格的 historical semantic retrieval,而是“latest recall + historical materialization”的近似。也就是说,它适合做可用的 time-travel read/search,但如果要严格复现过去某次检索结果,还需要记录当时的候选集、ranking score、query、index version 和注入结果。
这组三者要分清:
| 概念 | 关注点 | 在 memory 系统里的映射 |
|---|---|---|
| Event log | 发生了什么状态变化 | MemoryPatchGenerated、MemoryPatchApplied、MemoryDeleted、MemoryCompacted |
| Diff history | 如何从一个版本还原到另一个版本 | VERSION_HISTORY.reverse_diff |
| Projection | 给读路径用的当前视图 | latest memory file、vector index、overview、summary |
| Replay | 用历史事件或版本重建某个状态 | materialize_memory_at_version、rebuild index、offline eval |
Event Sourcing 的长期价值,是让状态变化可以被重放;OpenViking 的版本化方案优先解决的是“按版本读取/检索 memory 文件”。如果未来要支持更强的 replay / eval,还需要把 memory patch 的来源、生成策略、apply 策略、merge 决策和消费证据也记录成事件。
一个最小 schema 可以这样设计:
memory_event_log_v0:
event_id
event_type: source_session_committed | patch_generated | patch_applied | merge_required | conflict_retry | memory_deleted | compacted
source_session_id
memory_uri
src_data_version
head_data_version
applied_data_version
patch_id
read_set
write_set
merge_path
evaluator_delta
consumed_by_run_id
这里的关键不是把所有东西都做复杂,而是把 latest memory 从“唯一事实”降级为一个 projection。真正用于归因的是:
source trajectory
-> memory event
-> versioned policy view
-> exposure / consumption
-> outcome delta
这样才能回答几个重要问题:
- 某次任务执行时,agent 实际可见的是哪个 memory state?
- 一条经验是何时生成、何时 apply、何时第一次被消费的?
- 任务变好是因为 memory update,还是因为随机性 / 环境变化 / evaluator 漂移?
- 如果 memory 后来被改写,历史失败是否仍应按旧版本解释?
- Event Sourcing 不是说每次读取都必须从头 replay。生产系统通常会保存 latest projection / snapshot,只在审计、回放、迁移、debug 时回放事件。
search(data_version)如果只用最新向量召回,就不是严格的历史检索,只是低成本近似。严格历史检索要额外保存 index version 或 retrieval trace。- diff history 只说明文本如何还原,不说明语义上为什么改。要做 memory learning,还需要记录 patch reason、source trace、evaluator signal 和消费证据。
- 事件 replay 要压制外部副作用:重放时不能重新发消息、下单、调用外部写接口;只能重建状态或在 sandbox 中验证。
来源:Shapiro et al.: A comprehensive study of Convergent and Commutative Replicated Data Types。
CRDT(Conflict-free Replicated Data Type)的核心是:把数据类型设计成多副本异步更新后,即使没有前台同步协调,也能最终收敛到同一个状态。
它适合解决的是:对天然可交换、可合并的数据结构放宽同步要求,让副本先本地写入,再通过异步传播合并状态。代价是:并不是所有业务约束都能靠 CRDT 自动保证。
State-based CRDT 传播的是状态本身。状态集合需要构成 join-semilattice:
merge 操作是 least upper bound:
直觉上,x \sqcup y 是“刚好包含两个副本全部信息、且不多引入额外信息”的最小共同上界。只要每次本地 update 都让状态单调向上,并且 merge 满足下面三条性质,副本最终就会收敛:
典型例子:
- G-Set:只增集合,
merge = union。 - G-Counter:每个 replica 一个 counter slot,
merge = component-wise max,读值时求和。
Operation-based CRDT 传播的不是整个 state,而是 operation。只要所有副本最终收到操作,并且并发操作可以 commute,就能收敛。
它通常还需要 causal delivery:如果一个操作依赖另一个操作,那么依赖项必须先送达。否则副本可能先看到后续操作,却缺少解释它的因果前提。
CRDT 不是“无锁万能药”。它保证的是合并收敛,不等于保证所有业务约束都成立。
典型问题是 non-negative counter:两个副本本地都看到余额为 1,同时执行 decrement,各自本地都合法;异步合并后,全局结果可能变成 -1。这类“不小于 0”“库存不能超卖”“权限不能被并发绕过”的全局 invariant,通常仍需要同步、escrow / reservation、中心化 validation,或把约束重新设计成可组合的局部配额。
应用场景。 在 agent memory / eval 系统里,CRDT 更适合处理“天然可合并”的辅助状态,例如去重集合、计数器、tag 追加、观测事件集合;不适合直接处理需要全局排序、互斥决策、不可重复消费或严格版本边界的 memory patch 提交。后者更接近 OCC / Event Sourcing / versioned policy view 的问题。
《The Art of Readable Code》 by Dustin Boswell and Trevor Foucher. Copyright 2012 Dustin Boswell and Trevor Foucher, 978-0-596-80229-5
- Code should be written to minimize the time it would take for someone else to understand it.
-
Word Alternatives
- send: deliver, dispatch, announce, distribute, route
- find: search, extract, locate, recover
- start: launch, create, begin, open
- make: create, set up, build, generate, compose, add, new
-
Avoid Generic Names Like tmp and retval
sum_squares += v[i] * v[i];- The name tmp should be used only in cases when being short-lived and temporary is the most important fact about that variable
tmp_file
- loop iterators: ci, mi, ui
-
Prefer Concrete Names over Abstract Names
- ServerCanStart() -> CanListenOnPort()
#define DISALLOW_COPY_AND_ASSIGN(ClassName) ...
-
Attaching Extra Information to a Name
- delay_secs, size_mb, max_kbps, degrees_cw (cw means clockwise)
- untrustedUrl, plaintext_password, unescaped_comment, html_utf8, data_urlenc
- 拓展:Hungarian notation
- pszbuffer, z(zero-terminated)
-
How Long Should a Name Be?
- Shorter Names Are Okay for Shorter Scope
ConvertToString()->ToString()
-
Use Name Formatting to Convey Meaning
- kMaxOpenFile 方便和宏区分
- 私有成员加下划线后缀
static const int kMaxOpenFiles = 100;
class LogReader {
public:
void OpenFile(string local_file);
private:
int offset_;
DISALLOW_COPY_AND_ASSIGN(LogReader);
};- about HTML/CSS
- use underscores to separate words in IDs and dashes to separate words in classes
<div id="middle_column" class="main-content">
filter()->select()orexclude()Clip(text, length)->truncate(text, max_chars)- The clearest way to name a limit is to put
max_ormin_in front of the thing being limited. - when considering ranges
- Prefer first and last for Inclusive Ranges
- Prefer begin and end for Inclusive/Exclusive Ranges
- when using bool
read_password->need_passwordoruser_is_authenticated- avoid negated terms
HasSpaceLeft(), useisorhas
- Matching Expectations of Users, users may expect
get()orsize()to be lightweight methods.get_mean->compute_mean()list::size()不一定是O(1)
- Example: Evaluating Multiple Name Candidates
inherit_from_experiment_id:orcopy_experiment:
-
principles
- Use consistent layout, with patterns the reader can get used to.
- Make similar code look similar.
- Group related lines of code into blocks.
-
Rearrange Line Breaks to Be Consistent and Compact
public class PerformanceTester {
// TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
// [Kbps] [ms] [ms] [percent]
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(500, 80, 200, 1);
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(45000, 10, 0, 0);
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(100, 400, 250, 5);
}- Use Methods to Clean Up Irregularity
- If multiple blocks of code are doing similar things, try to give them the same silhouette.
void CheckFullName(string partial_name,
string expected_full_name,
string expected_error) {
// database_connection is now a class member
string error;
string full_name = ExpandFullName(database_connection, partial_name, &error); assert(error == expected_error);
assert(full_name == expected_full_name);
}- Use Column Alignment When Helpful
- Pick a Meaningful Order, and Use It Consistently
- Match the order of the variables to the order of the
inputfields on the corresponding HTML form. - Order them from “most important” to “least important.”
- Order them alphabetically.
- Match the order of the variables to the order of the
- Organize Declarations into Blocks
- Break Code into “Paragraphs”
def suggest_new_friends(user, email_password):
# Get the user's friends' email addresses.
friends = user.friends()
friend_emails = set(f.email for f in friends)
# Import all email addresses from this user's email account.
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)
# Find matching users that they aren't already friends with.
non_friend_emails = contact_emails - friend_emails
suggested_friends = User.objects.select(email__in=non_friend_emails)
# Display these lists on the page.
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends
return render("suggested_friends.html", display)- Personal Style versus Consistency
- Consistent style is more important than the “right” style.
The purpose of commenting is to help the reader know as much as the writer did.
- What NOT to Comment
- Don’t comment on facts that can be derived quickly from the code itself.
- Don’t Comment Just for the Sake of Commenting
- Don’t Comment Bad Names—Fix the Names Instead
# remove everything after the second '*'
name = '*'.join(line.split('*')[:2])// Find a Node with the given 'name' or return NULL.
// If depth <= 0, only 'subtree' is inspected.
// If depth == N, only 'subtree' and N levels below are inspected.
Node* FindNodeInSubtree(Node* subtree, string name, int depth);// Make sure 'reply' meets the count/byte/etc. limits from the 'request'
void EnforceLimitsFromRequest(Request request, Reply reply);
void ReleaseRegistryHandle(RegistryKey* key);- Recording Your Thoughts
- Include “Director Commentary”
- Comment the Flaws in Your Code
- Comment on Your Constants
// Surprisingly, a binary tree was 40% faster than a hash table for this data.
// The cost of computing a hash was more than the left/right comparisons.
// This heuristic might miss a few words. That's OK; solving this 100% is hard.
// This class is getting messy. Maybe we should create a 'ResourceNode' subclass to
// help organize things.// TODO: use a faster algorithm
// TODO(dustin): handle other image formats besides JPEG
// FIXME
// HACK
// XXX: Danger! Major problem here!
// todo: (lower case) or maybe-later:NUM_THREADS = 8; // as long as it's >= 2 * num_processors, that's good enough.
// Impose a reasonable limit - no human can read that much anyway.
const int MAX_RSS_SUBSCRIPTIONS = 1000;
image_quality = 0.72; // users thought 0.72 gave the best size/quality tradeoff- Put Yourself in the Reader’s Shoes
- Anticipating Likely Questions
- Advertising Likely Pitfalls
- “Big Picture” Comments
- Summary Comments
// Force vector to relinquish its memory (look up "STL swap trick")
vector<float>().swap(data);// Calls an external service to deliver email. (Times out after 1 minute.)
void SendEmail(string to, string subject, string body);
// Runtime is O(number_tags * average_tag_depth), so watch out for badly nested inputs.
def FixBrokenHtml(html): ...// This file contains helper functions that provide a more convenient interface to
// our file system. It handles file permissions and other nitty-gritty details.def GenerateUserReport():
# Acquire a lock for this user
...
# Read user's info from the database
...
# Write info to a file
...
# Release the lock for this user- Final Thoughts—Getting Over Writer’s Block
// Oh crap, this stuff will get tricky if there are ever duplicates in this list.
--->
// Careful: this code doesn't handle duplicates in the list (because that's hard to do)Comments should have a high information-to-space ratio.
- Keep Comments Compact
// CategoryType -> (score, weight)
typedef hash_map<int, pair<float, float> > ScoreMap;- Avoid Ambiguous Pronouns
// Insert the data into the cache, but check if it's too big first.
--->
// Insert the data into the cache, but check if the data is too big first.
--->
// If the data is small enough, insert it into the cache.-
Polish Sloppy Sentences
- e.g. Give higher priority to URLs we've never crawled before.
-
Describe Function Behavior Precisely
- e.g. Count how many newline bytes ('\n') are in the file.
-
Use Input/Output Examples That Illustrate Corner Cases
// ...
// Example: Strip("abba/a/ba", "ab") returns "/a/"
String Strip(String src, String chars) { ... }
// Rearrange 'v' so that elements < pivot come before those >= pivot;
// Then return the largest 'i' for which v[i] < pivot (or -1 if none are < pivot)
// Example: Partition([8 5 9 8 2], 8) might result in [5 2 | 8 9 8] and return 1
int Partition(vector<int>* v, int pivot);- State the Intent of Your Code
void DisplayProducts(list<Product> products) {
products.sort(CompareProductByPrice);
// Display each price, from highest to lowest
for (list<Product>::reverse_iterator it = products.rbegin(); it != products.rend(); ++it)
DisplayPrice(it->price);
...
}- “Named Function Parameter” Comments
void Connect(int timeout, bool use_encryption) { ... }
// Call the function with commented parameters
Connect(/* timeout_ms = */ 10, /* use_encryption = */ false);- Use Information-Dense Words
- // This class acts as a caching layer to the database.
- // Canonicalize the street address (remove extra spaces, "Avenue" -> "Ave.", etc.)
- The Order of Arguments in Conditionals
while (bytes_received < bytes_expected)
- The Order of if/else Blocks
- Prefer dealing with the positive case first instead of the negative—e.g., if (debug) instead of if (!debug).
- Prefer dealing with the simpler case first to get it out of the way. This approach might also allow both the if and the else to be visible on the screen at the same time, which is nice.
- Prefer dealing with the more interesting or conspicuous case first.
- The ?: Conditional Expression (a.k.a. “Ternary Operator”)
- By default, use an if/else. The ternary ?: should be used only for the simplest cases.
- Avoid do/while Loops
public boolean ListHasNode(Node node, String name, int max_length) {
while (node != null && max_length-- > 0) {
if (node.name().equals(name)) return true;
node = node.next();
}
return false;
}do {
continue;
} while (false);
// loop just once- Returning Early from a Function
- cleanup code
- C++: destructor
- Java, Python: try finally
- Python: with
- C#: using
- cleanup code
struct StateFreeHelper {
state* a;
StateFreeHelper(state* a) : a(a) {}
~StateFreeHelper() { free(a); }
};
void func(state* a) {
StateFreeHelper(a);
if (...) {
return;
} else {
...
}
}def do_stuff(self):
self.some_state = True
try:
# do stuff which may take some time - and user may quit here
finally:
self.some_state = False-
The Infamous goto
- 问题在于滥用,比如多种goto混合、goto到前面的代码
-
Minimize Nesting
- Removing Nesting by Returning Early
- Removing Nesting Inside Loops: use continue for independent iterations
-
Can You Follow the Flow of Execution?
- Explaining Variables
username = line.split(':')[0].strip()
if username == "root":
...- Summary Variables
final boolean user_owns_document = (request.user.id == document.owner_id);
if (user_owns_document) {
}
...
if (!user_owns_document) {
// document is read-only...
}- Using De Morgan’s Laws
- Abusing Short-Circuit Logic
- There is also a newer idiom worth mentioning: in languages like Python, JavaScript, and Ruby, the “or” operator returns one of its arguments (it doesn’t convert to a boolean), so code like: x = a || b || c, can be used to pick out the first “truthy” value from a, b, or c.
assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());
--->
bucket = FindBucket(key);
if (bucket != NULL) assert(!bucket->IsOccupied());- Example: Wrestling with Complicated Logic
struct Range {
int begin;
int end;
// For example, [0,5) overlaps with [3,8)
bool OverlapsWith(Range other);
};
bool Range::OverlapsWith(Range other) {
return (begin >= other.begin && begin < other.end) ||
(end > other.begin && end <= other.end) ||
(begin <= other.begin && end >= other.end);
}
bool Range::OverlapsWith(Range other) {
if (other.end <= begin) return false; // They end before we begin
if (other.begin >= end) return false; // They begin after we end
return true; // Only possibility left: they overlap
}-
Breaking Down Giant Statements
-
Another Creative Way to Simplify Expressions
void AddStats(const Stats& add_from, Stats* add_to) {
#define ADD_FIELD(field) add_to->set_##field(add_from.field() + add_to->field())
ADD_FIELD(total_memory);
ADD_FIELD(free_memory);
ADD_FIELD(swap_memory);
ADD_FIELD(status_string);
ADD_FIELD(num_processes);
...
#undef ADD_FIELD
}- Eliminating Variables
- Useless Temporary Variables
- Eliminating Intermediate Results
- Eliminating Control Flow Variables
- Shrink the Scope of Your Variables
- Another way to restrict access to class members is to make as many methods static as possible. Static methods are a great way to let the reader know “these lines of code are isolated from those variables.”
- break the large class into smaller classes
- if Statement Scope in C++
- Creating “Private” Variables in JavaScript
- JavaScript Global Scope
- always define variables using the var keyword (e.g., var x = 1)
- No Nested Scope in Python and JavaScript
- 在最近祖先手动定义 xxx = None
- Moving Definitions Down
if (PaymentInfo* info = database.ReadPaymentInfo()) {
cout << "User paid: " << info->amount() << endl;
}var submit_form = (function () {
var submitted = false; // Note: can only be accessed by the function below
return function (form_name) {
if (submitted) {
return; // don't double-submit the form
}
...
submitted = true;
};
}());- Prefer Write-Once Variables
- The more places a variable is manipulated, the harder it is to reason about its current value.
- A Final Example
var setFirstEmptyInput = function (new_value) {
for (var i = 1; true; i++) {
var elem = document.getElementById('input' + i);
if (elem === null)
return null; // Search Failed. No empty input found.
if (elem.value === '') {
elem.value = new_value;
return elem;
}
}
};- Introductory Example: findClosestLocation()
- Pure Utility Code
- read file to string
- Other General-Purpose Code
var format_pretty = function (obj, indent) {
// Handle null, undefined, strings, and non-objects.
if (obj === null) return "null";
if (obj === undefined) return "undefined";
if (typeof obj === "string") return '"' + obj + '"';
if (typeof obj !== "object") return String(obj);
if (indent === undefined) indent = "";
// Handle (non-null) objects.
var str = "{\n";
for (var key in obj) {
str += indent + " " + key + " = ";
str += format_pretty(obj[key], indent + " ") + "\n";
}
return str + indent + "}";
};-
Create a Lot of General-Purpose Code
-
Project-Specific Functionality
CHARS_TO_REMOVE = re.compile(r"['\.]+")
CHARS_TO_DASH = re.compile(r"[^a-z0-9]+")
def make_url_friendly(text):
text = text.lower()
text = CHARS_TO_REMOVE.sub('', text)
text = CHARS_TO_DASH.sub('-', text)
return text.strip("-")
business = Business()
business.name = request.POST["name"]
business.url = "/biz/" + make_url_friendly(business.name)
business.date_created = datetime.datetime.utcnow()
business.save_to_database()- Simplifying an Existing Interface
- Reshaping an Interface to Your Needs
def url_safe_encrypt(obj):
obj_str = json.dumps(obj)
cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = cipher.update(obj_str)
encrypted_bytes += cipher.final() # flush out the current 128 bit block
return base64.urlsafe_b64encode(encrypted_bytes)- Taking Things Too Far
- Tasks Can Be Small
- e.g. 分解 old vote 和 new vote
- Extracting Values from an Object
var first_half, second_half;
if (country === "USA") {
first_half = town || city || "Middle-of-Nowhere";
second_half = state || "USA";
} else {
first_half = town || city || state || "Middle-of-Nowhere";
second_half = country || "Planet Earth";
}
return first_half + ", " + second_half;- A Larger Example
- Describing Logic Clearly
- “rubber ducking”
- You do not really understand something unless you can explain it to your grandmother. —Albert Einstein
if (is_admin_request()) {
// authorized
} elseif ($document && ($document['username'] == $_SESSION['username'])) {
// authorized
} else {
return not_authorized();
}
// continue rendering the page ...- Knowing Your Libraries Helps
- Applying This Method to Larger Problems
def PrintStockTransactions():
stock_iter = ...
price_iter = ...
num_shares_iter = ...
while True:
time = AdvanceToMatchingTime(stock_iter, price_iter, num_shares_iter)
if time is None:
return
# Print the aligned rows.
print "@", time,
print stock_iter.ticker_symbol,
print price_iter.price,
print num_shares_iter.number_of_shares
stock_iter.NextRow()
price_iter.NextRow()
num_shares_iter.NextRow()
def AdvanceToMatchingTime(row_iter1, row_iter2, row_iter3):
while row_iter1 and row_iter2 and row_iter3:
t1 = row_iter1.time
t2 = row_iter2.time
t3 = row_iter3.time
if t1 == t2 == t3:
return t1
tmax = max(t1, t2, t3)
# If any row is "behind," advance it.
# Eventually, this while loop will align them all.
if t1 < tmax: row_iter1.NextRow()
if t2 < tmax: row_iter2.NextRow()
if t3 < tmax: row_iter3.NextRow()
return None # no alignment could be found-
Don’t Bother Implementing That Feature—You Won’t Need It
-
Question and Break Down Your Requirements
- Example: A Store Locator ---- For any given user’s latitude/longitude, find the store with the closest latitude/longitude.
- When the locations are on either side of the International Date Line
- When the locations are near the North or South Pole
- Adjusting for the curvature of the Earth, as “longitudinal degrees per mile” changes
- Example: Adding a Cache
- Example: A Store Locator ---- For any given user’s latitude/longitude, find the store with the closest latitude/longitude.
-
Keeping Your Codebase Small
-
Be Familiar with the Libraries Around You
- Example: Lists and Sets in Python
-
Example: Using Unix Tools Instead of Coding
- When a web server frequently returns 4xx or 5xx HTTP response codes, it’s a sign of a potential problem (4xx being a client error; 5xx being a server error).
-
testing中的一些概念:
- 单测:单元性和隔离性
- property-based testing 常用于单测,也可以作用于组件、状态机或 API;它是一种输入生成与 invariant 验证方法,不是固定的测试层级
- 完整的测试层次、Gherkin、coverage 与 mutation testing 见软件测试与质量保障
-
Make Tests Easy to Read and Maintain
-
What’s Wrong with This Test?
void CheckScoresBeforeAfter(string input, string expected_output) {
vector<ScoredDocument> docs = ScoredDocsFromString(input);
SortAndFilterDocs(&docs);
string output = ScoredDocsToString(docs);
assert(output == expected_output);
}
vector<ScoredDocument> ScoredDocsFromString(string scores) {
vector<ScoredDocument> docs;
replace(scores.begin(), scores.end(), ',', ' ');
// Populate 'docs' from a string of space-separated scores.
istringstream stream(scores);
double score;
while (stream >> score) {
AddScoredDoc(docs, score);
}
return docs;
}
string ScoredDocsToString(vector<ScoredDocument> docs) {
ostringstream stream;
for (int i = 0; i < docs.size(); i++) {
if (i > 0) stream << ", ";
stream << docs[i].score;
}
return stream.str();
}- Making Error Messages Readable
- Python
import unittest
- Python
BOOST_REQUIRE_EQUAL(output, expected_output)-
Choosing Good Test Inputs
- In general, you should pick the simplest set of inputs that completely exercise the code.
- Simplifying the Input Values
- -1e100、-1
- it’s more effective to construct large inputs programmatically, constructing a large input of (say) 100,000 values
-
Naming Test Functions
-
What Was Wrong with That Test?
-
Test-Friendly Development
- Test-driven development (TDD)
- Table 14.1: Characteristics of less testable code
- Use of global variables ---> gtest set_up()
- Code depends on a lot of external components
- Code has nondeterministic behavior
-
Going Too Far
- Sacrificing the readability of your real code, for the sake of enabling tests.
- Being obsessive about 100% test coverage.
- Letting testing get in the way of product development.
- Defining the Class Interface
// Track the cumulative counts over the past minute and over the past hour.
// Useful, for example, to track recent bandwidth usage.
class MinuteHourCounter {
// Add a new data point (count >= 0).
// For the next minute, MinuteCount() will be larger by +count.
// For the next hour, HourCount() will be larger by +count.
void Add(int count);
// Return the accumulated count over the past 60 seconds.
int MinuteCount();
// Return the accumulated count over the past 3600 seconds.
int HourCount();
};- Attempt 1: A Naive Solution
- list, reverse_iterator,效率低
- Attempt 2: Conveyor Belt Design
- 两个传送带,内存消耗大,拓展成本高
- Attempt 3: A Time-Bucketed Design
- 本质利用了统计精度可牺牲的特点,离散化实现
// A class that keeps counts for the past N buckets of time.
class TrailingBucketCounter {
public:
// Example: TrailingBucketCounter(30, 60) tracks the last 30 minute-buckets of time.
TrailingBucketCounter(int num_buckets, int secs_per_bucket);
void Add(int count, time_t now);
// Return the total count over the last num_buckets worth of time
int TrailingCount(time_t now);
};
class ConveyorQueue;