当前位置: 首页 > news >正文

技术架构文档的写作规范:ADRs(Architecture Decision Records)从原理到实践

技术架构文档的写作规范:ADRs(Architecture Decision Records)从原理到实践

一、架构知识流失——为什么"口口相传"的架构决策必然失败

技术团队的架构知识流失是一种慢性病。一个关键的架构决策——为什么选择了消息队列A而非B、为什么数据库表设计采用反范式化、为什么微服务拆分以这个边界为准——如果只存在于当事人的头脑中或某次会议的纪要里,随着人员流动和时间推移,这些决策的"为什么"将彻底消失。

后果是双重的:

  1. 新人困惑:新成员看到代码中的非直觉设计,无法判断这是"刻意的架构权衡"还是"历史的遗留债务"。前者需要维护,后者需要重构。
  2. 决策反复:当原有决策的条件发生变化(如流量增长10倍、数据量突破阈值),团队缺乏上下文来判断原决策的前提是否依然成立,导致在同样的问题上重复讨论。

ADRs(Architecture Decision Records)正是为解决这一问题而生的轻量级文档范式。它不追求架构文档的系统性和完整性,而是聚焦于记录单个决策的上下文、方案对比、选择理由和后果。一个ADRs文件短则一页,长则三四页,但信息密度极高——读者能在5分钟内理解一个关键决策的来龙去脉。

二、ADRs的核心结构——从Michael Nygard模板到团队自定义规范

ADRs 的经典模板由 Michael Nygard 在2011年提出,包含五个核心章节。一个良好的 ADR 文档具有极高的信息密度和自包含特性。

标题规范:采用ADR-NNNN: 决策的简要描述格式。编号使用自增序列,不重新编排。标题应当概括"做什么决策",如"ADR-0003: 采用PostgreSQL的JSONB列存储非结构化元数据",而非"关于数据库选型的讨论"。

状态机是ADR的生命周期管理核心。状态流转规则:

  • Draft → Proposed:初稿完成,提交团队评审
  • Proposed → Accepted:团队达成共识,ADR成为正式记录
  • Accepted → Superseded:新决策取代旧决策,ADR头部引用新ADR编号
  • Accepted → Deprecated:决策因架构演进自然过期(如微服务下线)

背景章节要回答"为什么现在做这个决策"和"如果不做决定会怎样"。不是描述技术方案的细节,而是描述面临的具体技术约束和业务需求。比如:"当前的MySQL单表已增长到5000万行,按月分区策略导致跨月查询需要扫描所有分区,p99延迟已恶化到3秒。"

决策章节需要阐述"我们决定做什么"以及"为什么是这个方案"。关键不是罗列方案特性,而是说明权衡(trade-off)的逻辑——为什么接受A的成本换取B的收益。如果列出了备选方案,需要逐一说明为什么不选。

后果章节是ADR最有价值的部分。它分为两个方向:

  • 正向:哪些问题被解决了,什么变得更容易
  • 负向:引入了什么新成本,什么变得更困难,需要注意哪些风险

三、ADR管理工具——模板驱动的规范化生成与检索

以下代码实现了一套ADR生命周期管理工具,确保团队以统一格式生成和管理架构决策记录:

""" ADR(Architecture Decision Records)管理工具 支持模板生成、状态管理、关联检索和Markdown渲染 """ from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Optional from enum import Enum class ADRStatus(str, Enum): """ADR状态枚举""" DRAFT = "draft" PROPOSED = "proposed" ACCEPTED = "accepted" SUPERSEDED = "superseded" DEPRECATED = "deprecated" @property def label(self) -> str: labels = { "draft": "草案", "proposed": "提议中", "accepted": "已接受", "superseded": "已取代", "deprecated": "已废弃", } return labels[self.value] @dataclass class ADR: """ADR数据模型""" id: int title: str status: ADRStatus context: str # 背景 decision: str # 决策 consequences: str # 后果 alternatives: list[str] = field(default_factory=list) superseded_by: Optional[int] = None supersedes: Optional[int] = None created_at: datetime = field(default_factory=datetime.now) updated_at: datetime = field(default_factory=datetime.now) tags: list[str] = field(default_factory=list) def filename(self) -> str: """生成标准文件名""" slug = ( self.title.lower() .replace(" ", "-") .replace("(", "") .replace(")", "") ) return f"ADR-{self.id:04d}-{slug}.md" class ADRGenerator: """ADR文档生成器:从数据模型生成Markdown格式文档""" TEMPLATE = """# {title} - **状态**: {status} - **日期**: {date} - **标签**: {tags} {superseded_section}{supersedes_section} ## 背景与上下文 {context} ## 决策 {decision} ## 备选方案 {alternatives} ## 后果与影响 {consequences} """ def generate(self, adr: ADR) -> str: tags_str = ", ".join(adr.tags) if adr.tags else "—" superseded = "" if adr.superseded_by: superseded = f"- **已被取代**: ADR-{adr.superseded_by:04d}\n" supersedes = "" if adr.supersedes: supersedes = f"- **取代**: ADR-{adr.supersedes:04d}\n" alternatives_text = "" if adr.alternatives: items = [ f"- **备选{i+1}**: {alt}" for i, alt in enumerate(adr.alternatives) ] alternatives_text = "\n".join(items) else: alternatives_text = "(未记录备选方案)" return self.TEMPLATE.format( title=adr.title, status=adr.status.label, date=adr.updated_at.strftime("%Y-%m-%d"), tags=tags_str, superseded_section=superseded, supersedes_section=supersedes, context=adr.context.strip(), decision=adr.decision.strip(), alternatives=alternatives_text, consequences=adr.consequences.strip(), ) class ADRRepository: """ ADR仓库管理器 负责ADR的CRUD操作、索引管理和关联查询 """ INDEX_HEADER = """# 架构决策记录索引 | 编号 | 标题 | 状态 | 日期 | |------|------|------|------| """ def __init__(self, root_dir: str = "docs/adr"): self.root = Path(root_dir) self.root.mkdir(parents=True, exist_ok=True) self.generator = ADRGenerator() self._adrs: dict[int, ADR] = {} self._load_existing() def create(self, adr: ADR) -> str: """创建新的ADR""" if adr.id in self._adrs: raise ValueError(f"ADR-{adr.id:04d} 已存在") # 分配编号 if adr.id == 0: adr.id = max(self._adrs.keys(), default=0) + 1 content = self.generator.generate(adr) filepath = self.root / adr.filename() filepath.write_text(content, encoding="utf-8") self._adrs[adr.id] = adr self._rebuild_index() return str(filepath) def supersede(self, old_id: int, new_adr: ADR) -> tuple[str, str]: """ 用新ADR取代旧ADR 自动更新旧ADR的状态和引用 """ if old_id not in self._adrs: raise ValueError(f"ADR-{old_id:04d} 不存在") old_adr = self._adrs[old_id] old_adr.status = ADRStatus.SUPERSEDED old_adr.superseded_by = new_adr.id new_adr.supersedes = old_id # 重新生成旧ADR self._save_adr(old_adr) # 创建新ADR new_path = self.create(new_adr) self._rebuild_index() return (str(self.root / old_adr.filename()), new_path) def get_active_adrs(self) -> list[ADR]: """获取所有活跃(已接受)的ADR""" return [a for a in self._adrs.values() if a.status == ADRStatus.ACCEPTED] def search_by_tag(self, tag: str) -> list[ADR]: """按标签检索ADR""" return [a for a in self._adrs.values() if tag in a.tags] def get_decision_chain(self, start_id: int) -> list[ADR]: """追踪决策链:从一个ADR出发,找出所有取代它的后续ADR""" chain = [] current_id = start_id visited = set() while current_id and current_id not in visited: visited.add(current_id) current = self._adrs.get(current_id) if not current: break chain.append(current) current_id = current.superseded_by return chain def _load_existing(self) -> None: """加载已有ADR文件""" for md_file in sorted(self.root.glob("ADR-*.md")): content = md_file.read_text(encoding="utf-8") adr = self._parse_adr(content, md_file.stem) if adr: self._adrs[adr.id] = adr def _parse_adr(self, content: str, filename: str) -> Optional[ADR]: """从Markdown内容解析ADR""" lines = content.split("\n") if not lines: return None title_line = lines[0].strip("# ") adr_id = 0 try: parts = filename.split("-") if len(parts) >= 2: adr_id = int(parts[1]) except ValueError: pass return ADR( id=adr_id, title=title_line, status=ADRStatus.ACCEPTED, context="", decision="", consequences="", ) def _save_adr(self, adr: ADR) -> str: """更新已有ADR文件""" adr.updated_at = datetime.now() content = self.generator.generate(adr) filepath = self.root / adr.filename() filepath.write_text(content, encoding="utf-8") self._adrs[adr.id] = adr return str(filepath) def _rebuild_index(self) -> None: """重建README索引""" rows = [] for adr in sorted(self._adrs.values(), key=lambda a: a.id): date_str = adr.updated_at.strftime("%Y-%m-%d") rows.append(f"| ADR-{adr.id:04d} | {adr.title} | {adr.status.label} | {date_str} |") index_content = self.INDEX_HEADER + "\n".join(rows) + "\n" (self.root / "README.md").write_text(index_content, encoding="utf-8") # ========== 使用示例 ========== if __name__ == "__main__": repo = ADRRepository(root_dir="./docs/adr") # 创建第一条ADR adr1 = ADR( id=0, # 自动分配 title="ADR-0001: 采用PostgreSQL的JSONB列存储非结构化元数据", status=ADRStatus.ACCEPTED, context=( "产品中的每个订单有10-50个动态扩展属性(如定制参数)," "传统EAV模式需要3张表联表查询,查询复杂度高。" "业务要求单次查询延迟 <200ms。" ), decision=( "使用PostgreSQL 14的JSONB数据类型存储动态扩展属性," "配合GIN索引加速JSON路径查询," "使用CHECK约束确保JSON Schema合规。" ), alternatives=[ "EAV(实体-属性-值)模式:灵活但查询复杂,不满足延迟要求", "MongoDB:需要引入新数据库组件,运维成本过高", "MySQL 8.0 JSON类型:支持不完善,性能弱于PG", ], consequences=( "正向:单表查询即可获取订单全量数据,延迟满足要求。\n" "负向:JSONB索引占用较多存储空间(约原始数据的30%)," "需要建立JSON Schema治理规范," "开发团队需了解PG的JSON操作符。" ), tags=["database", "postgresql", "data-modeling"], ) path1 = repo.create(adr1) print(f"已创建: {path1}") # 获取所有活跃ADR active = repo.get_active_adrs() print(f"活跃ADR数量: {len(active)}") # 检索标签 db_adrs = repo.search_by_tag("database") print(f"数据库类ADR数量: {len(db_adrs)}")

四、ADR实践中的反模式——什么不算ADR,什么不该记录为ADR

不要将ADR写成架构文档的替代品。ADR记录的是决策,不是系统全景图。架构文档(C4 Model等)描述系统是什么样,ADR描述为什么是这个样。两者互补,不是竞争关系。

不要记录所有技术决策。只有以下类型的决策值得写入ADR:

  • 影响系统外部接口或行为(API契约、协议选择)
  • 产生显著的技术债务或运维成本(引入新技术栈、数据存储方案)
  • 会约束未来的架构选择(微服务拆分边界、Event Schema)
  • 存在多个合理的方案且团队有分歧

不要在ADR中堆砌无关细节。一个ADR应该在300-800字内完成核心内容的阐述。如果超过1500字,说明决策本身需要进一步拆解为多个更小的决策。

不要忘记更新ADR的状态。ADR不是一次性的交付物。当架构演进导致原有决策不再适用时,必须通过supersededdeprecated状态显式标记,确保读者看到的是"当前有效的约定"而非"历史的考古记录"。

五、总结

ADRs是一种轻量级架构决策记录格式,通过背景-决策-后果的三段式结构,将关键架构决策的"为什么"固化到代码仓库中。其核心价值不在于文档本身,而在于迫使团队在做出决策时显式记录上下文、评估方案、声明后果——这个过程本身就是架构决策质量的保障机制。

落地要点:

  1. 从零开始:选最近一次有分歧的技术决策,写出第一篇ADR
  2. 与代码同行:ADR存放在代码仓库的docs/adr/目录中,随PR提交
  3. 轻量渐进:不追求全覆盖,每月新增1-2条高质量的ADR比一次性"补齐"所有历史决策更有价值
  4. 状态维护:定期回顾已接受的ADR,评估其是否依然适用

ADRs解决的不是"文档怎么写"的问题,而是"团队如何积累和传承架构判断力"的问题——这是技术团队从短期生存走向长期演进的必经之路。

http://www.jsqmd.com/news/1201671/

相关文章:

  • Redis 3.x——哨兵基本概念
  • 如何在Apple Silicon上部署Qwopus3.6-35B-A3B-Coder-6bit:从安装到运行的完整教程
  • Rainbow-delimiters性能测试报告:与其他彩虹括号插件的对比
  • AI产品用户旅程的转化漏斗模型:从首次激活到深度留存的工程化分析
  • 小程序计算机毕设之文旅展馆线上预约服务系统的设计与实现 基于 Django + 微信小程序的咖啡博物馆票务管理小程序(完整前后端代码+说明文档+LW,调试定制等)
  • Ornith-1.0-9B-OptiQ-4bit部署实战:从安装到运行的完整流程
  • uos-mgmt-exporter与Prometheus+Grafana集成:打造可视化监控平台
  • 怎么批量下载图片
  • 如何通过FanControl实现华硕主板风扇的精准控制与静音优化
  • mysql日志刷盘
  • Redis 3.x——哨兵安装和部署
  • 网闸工作原理
  • AnimeGen-I2V-A14B-Lightning-int4部署指南:从本地到云端的最佳实践
  • 2026龙虾AI智能体平台哪家好:七款本地云端隐私自动化工具横向测评
  • 未来已来:Dolphin3-Cyber-8B-GGUF如何重塑网络安全培训与攻防演练
  • 3步搞定音频转换:fre:ac开源音频转换器的完整指南
  • 小程序计算机毕设之基于 Django 与协同过滤算法的校园音乐推荐小程序的设计与实现(完整前后端代码+说明文档+LW,调试定制等)
  • Copilot键盘操作全图解:从新手到专家必须掌握的15个隐藏快捷键
  • DAVIS346动态视觉传感器
  • React Komposer 数据容器设计原理深度解析:构建高效React应用的核心秘诀
  • 如何快速上手MiniCPM5-1B-Agentic-Tooluse-GGUF:从下载到运行的完整指南
  • 源码交付、禁止转包、AI原生架构:2026年软件定制开发避坑指南(附西安服务商深度测评)
  • 革命性AI视觉语言模型:mlx-community/gemma-4-e4b-it-nvfp4全面解析与快速上手指南
  • Mysql会产生几种日志
  • C++固定块内存池实现:无碎片化内存管理原理与实战
  • 扑克牌游戏各种算法优劣
  • LT8306RS6#TRMPBF是一款无光耦 60V 微功耗隔离反激控制器
  • Qwopus3.6-35B-A3B-Coder-4bit:革命性4bit量化多模态模型,在Apple Silicon上轻松实现AI编码与视觉分析
  • Gittle分支管理:Python中创建、切换和删除分支的终极指南
  • 【射频入门】PA和LNA测试到底有啥区别?一文搞懂大信号vs小信号,防烧才是硬道理。