⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 技术琐话 「Malte」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

作者:Malte是Vercel的CTO。在此之前,Malte是负责谷歌搜索渲染的首席工程师,以及Search on Laptops, Tablets, 和Desktop的工程总监。

译者,许晓斌,现任阿里巴巴资深技术专家,《Maven实战》作者。

Google 软件工程文化中关键元素之一,是使用设计文档来定义软件设计。这些文档通常不是非常正式,主要是软件系统或应用程序的作者在着手写代码之前编写的。这些设计文档记录了 high-level 的实现策略和关键的设计决策,而后者重点描述了决策过程中思考的权衡。

作为软件工程师,我们的工作并不是生产代码本身,而是解决问题。非结构化的文本,例如以设计文档的形式,在项目早期或许是解决问题更适宜的工具。因为设计文档可能更简明、更容易被理解,相比代码能在更高的层面沟通问题及解决方案。

除了用作软件设计的原始文档记录,设计文档还在软件研发生命周期中实现了下述功能:

●在修改成本还比较低的时候,尽早地识别设计缺陷。

●在组织中围绕设计达成共识。

●确保横切关注点(Cross-cutting concern)得到充分考虑。

●在组织中传播资深工程师的知识。

●就设计决策形成组织记忆基础。

●成为软件设计者技术资产中的一个摘要性制品。

1. 设计文档的构成

设计文档是非正式的文档,因此其内容不需要遵循严格的准则。因此首要原则就是:在特定的项目中,用任何最合理的形式编写。

在此原则之外,Google 也建立了一种颇有效果的设计文档结构。

1.1 上下文和范围(Context and scope)

这一小节给读者展现一个有关这个系统在哪里被构建,以及什么会被构建的非常粗的概览。这不是需求文档。保持言简意赅!这里的目标是让读者快速进入状态,可以假设读者知道一些前置的知识,相关详情可以给到链接。这一节内容应该完全关注在客观的背景事实。

1.2 目标和非目标(Goals and non-goals)

给出一个简单的列表,讲述系统的目标是什么。有时候更重要的是讲述非目标是什么。注意,非目标不是对目标的否定,例如“系统不应该 crash”,而是显示地挑选出来的不是目标的内容。一个好的例子是 “遵循 ACID”,当设计一个数据库的时候,你必然想要知道这是目标还是非目标。进一步的,你仍然可以选择一个方案来实现非目标,只要它不会给实现目标带来不必要的权衡。

1.3 实际设计(The actual design)

这一部分应该以一个概述开头,然后逐渐展开细节。

设计是文档是在你设计软件的过程中,记录设计取舍的地方。应该关注这些取舍,以产出一个具备长期价值的文档。具体就是,在既定的上下文(事实),目标和非目标(需求)下,设计文档应该提出解决方案,并阐明为什么某个特定的解决方案是满足这些目标的最佳方案。

相较于更为正式媒体形式,编写文档的意义在于可以用合适的方式灵活地表述手头的问题集合。因此,如何描述设计并没有显式的指引。

话虽如此,对于大多数设计文档来说,一些最佳实践和重复出现的主题还是有意义的:

1.3.1 系统上下文图(System-context-diagram)

对于很多文档来说,系统上下文图是很有用的。这类图展示了当前系统是更大技术图景的一部分,能让读者在一个他们已经熟悉的上下文环境中去理解新的设计。

系统上下文图的例子

1.3.2 APIs

如果设计的系统会暴露 API,那么草拟出 API 通常是好想法。不过,在大多数情况,我们应该克制住把正式接口和数据定义复制粘贴到文档中的冲动,因为这么做会导致文档过于冗长,包含不必要的细节,并很快过期。相对应的,我们应当关注和设计及取舍相关的那部分 API。

1.3.3 数据存储(Data storage)

需要存储系统的系统应该讨论数据是如何以何种形式,如何被存储的。和前面描述 API 的建议一样,基于相同的理由,应该避免复制粘贴完整的 schema 定义,正确的做法是关注在那些和设计取舍相关的部分。

1.3.4 代码和伪代码(Code and pseudo-code)

设计文档应当很少包含代码或伪装代码,除非有一些情况需要描述新的算法。合理的做法是给到设计原型实现的链接。

1.3.5 约束条件(Degree of constraint)

软件设计形态(因此设计文档)的主要影响因素是解决方案空间(solution space)中的约束条件。

一个方向的极端情况是 “绿地软件项目(greenfield)”,在这种情况下我们知道所有的目标,解决方案只要是合理的,没什么限制。这样的文档可能就会显得很宽泛,但是也应该快速定义一组规则,以便让大家尽快把目光收敛到一组可控的解决方案中。

另一个方向的极端情况是,所有可能的解决方案都被定义得很清楚了,但是如何把它们结合起来以达成目标,却毫不清晰。这往往是因为遗留系统难以改动,或者遗留系统不是被设计用来解决当前所面临的问题的,又或者是一个类库的设计要求我们在它的宿主编程语言限制下工作。

在这种情况下,你或许可以遍历所有可行的简单方法,但更需要创新地把所有这些方法整合起来以完成目标。也许存在多种方案,每一种方案都不是特别出色的,因此文档应该关注在如何从已经识别的各项取舍中,选择最合适的方案。

1.4 候选设计(Alternatives considered)

这一小节列出那些同样可以实现类似产出的可选设计。这里关注的应该是各种方案各自的取舍,以及这些取舍的对比如何引向最终的设计 —— 文档的核心主题。

虽然描述候选设计可以简洁一些,但是这一小节实际上非常核心的,因为这里非常清晰地展示了,在给定的项目目标和所有可选方案下,为什么选择了最终方案,在给定目标下权衡的判断是如何做出的,而这正是文档的读者所关注的核心。

1.5 横切关注点(Cross-cutting Concerns)

在这里组织可以确保一些横切关注点如安全,隐私,可观测性,总是被考虑到。这部分内容通常相对较短,只是用来解释设计会如何影响到横切关注点,以及相关影响如何得到解决。团队应该标准化在他们场景下的关注点。

例如,由于隐私非常重要,Google 的项目就必须写一个专门的隐私设计文档,这个文档会别专门用来 Review 隐私和安全。虽然 Review 只要在项目启动前完成,但通常最好是尽早让隐私和安全团队介入,确保设计从一开始就重视他们的意见。关于这部分内容更专门的细节,核心文档不一定要全部包含,有时候给到这些专门文档的引用即可。

1.6 设计文档的长度(The length of a design doc)

设计文档应该具备充分的细节,但同时足够简短以能够被忙碌的人阅读。对于一个大型项目来说,最佳的长度似乎是 10 到 20 页。如果你的内容超过这个大小,更合理的做法可能是把这个问题划分成更易管理的子问题。当然,需要注意的是,编写 1-3 页的“迷你设计文档”完全是可能的。这类文档对于敏捷项目中的增量改进或者子任务尤其有用 —— 但你仍然需要和编写长文档一样执行一样的步骤,区别只是让内容更精炼,并且只关注有限的问题集合。

2. 什么时候不要编写设计文档(When not to write a design doc)

编写设计文档是需要成本的。关于是否编写设计文档的决定,实际上是在做权衡。权衡的一边是围绕设计、文档、高层评审等工作形成组织共识的益处,权衡的另一边是这块工作投入的精力成本。决策的核心在于设计问题的解决方案是否模糊 —— 这往往是因为问题复杂度,或者解决方案的复杂度引起的(或者两者皆有)。如果不存在这个问题,那么走一个设计文档编写的流程价值就有限。

设计文档可能没有必要的一个明显征兆是:设计文档实际上是实现手册。如果文档基本上说的是“这是我们将如何实现之”,而没有深入讨论取舍、可选方案、没有解释决策(或者说解决方案太明显了以致于没什么取舍可讨论),那么很有可能直接编写真实代码是更好的选择。

最后,创建和评审设计文档的投入可能与快速制作原型并迭代理念并不相容。但是大多数软件项目都是有一组实际上已知的问题。拥抱敏捷方法不应该成为不花时间去寻找已知问题正确解决方案的借口。此外,原型本身可能就是创建设计文档过程的一部分。“我试过了,这么做可行” 就是选择一种设计最好的论据之一。

3. 设计文档的生命周期(The design doc lifecycle)

一份设计文档的生命周期包括如下阶段:

1.创建和快速迭代

2.审查(可能有多轮)

3.实现和迭代

4.维护和学习

3.1 创建和快速迭代(Creation and rapic iteration)

你编写文档,有时候是和几个合作者一起编写。

这一阶段快速演进成为快速迭代的时期,文档被分享给一些同事,他们拥有关于问题空间(problem space)的最多的知识(通常属于同一个团队),通过他们不断澄清问题并提出建议,文档逐渐形成第一个相对稳定的版本。

你会发现很多工程师和团队更偏向于使用版本控制和代码审查工具来管理文档,但是 Google 的大多数文档是使用 Google Docs 创建的,并重度使用了协作特性。

3.2 评审(Review)

在评审阶段,设计文档被分享给原始作者和紧密协作者之外更广范的一批受众。评审可以给文档增加很多价值,但是也有可能是危险的投入成本陷阱,因此要明智对待。

评审可以有很多形式:最轻量的版本就是简单把文档发给更大范围的团队,让大伙有机会可以看一眼。随着而来的讨论主要就发生在文档的评论区。而形式较重的评审,就是发起正式的设计文档评审会议,在会议中作者面向通常是较为资深的工程师听众演示文档(通常是专门的演示)。Google 有很多团队为此目的排了周期性的会议,工程师可以注册用来发起设计评审。自然的,等待此类会议来评审设计文档会大幅降低研发速度。工程师可以通过直接向同事获取最关键的反馈,同时也不阻塞更广泛的评审,进而降低这一风险。

当 Google 是一个小一些的公司的时候,大家习惯上会把设计发到一个中心的邮件列表中,在这里资深的工程师会抽空进行评审。这种方式对公司来说可能就不错。这种方式一大好处是,它在整个公司层面建立了一种相对一致的软件设计文化。但是随着公司逐渐增长细形成一个较大的工程师团队,维护这种中心化的方法就不可行了。

设计评审增加的主要价值是,它形成了一个让组织的综合经验可以融合到设计中的机会。如何让设计能够充分考虑横切关注点如可观测性、安全性、以及隐私,这一点就能够非常一致地在评审阶段得到保障。评审的主要价值不是问题被发现本身,而是让问题在软件研发阶段相对早期的时候,也就是修复成本相对较低的时候被发现。

3.3 实现和迭代(Implementation and iteration)

当事情获得了充分的进展,看起来进一步的评审不太会要求设计做重大的改变,那就是时候开始实现了。当计划和现实冲突,不可避免的会发现设计的缺陷,未被充分考虑的需求,或者基于经验的推测实际上错误的,进而发现需要对设计做修改。在这种情况下强烈推荐更新设计文档,一般说来:如果系统还没有上线,那么很确定应该更新文档。在实践中,我们普通人在更新文档方面都做得不好,以及因为一些其他的实际因素,更新还通常会落到新的独立的文档中去。这就导致了一种近似美国宪法的最终态:有一堆修正案,而非一份一致的文档。对于将来维护系统的可怜程序员来说,当他们像考古学家一样翻阅历史设计文档,去试图理解目标系统的时候,原始文档中给出这些“修正案”的链接会非常有帮助。

3.4 维护和学习(Maintenance and learning)

当 Google 工程师首次接触一个系统的时候,他们的第一个问题通常是“设计文档在哪里?”。虽然说设计文档和所有其他文档一样,都会随着时间的流逝变得和现实不一致,但它们依旧是学习系统在创建之初其背后思考的最佳起步材料。

作为作者,从为自己着想的角度考虑,可以在一两年后重新阅读自己的设计文档。你哪里做对了?哪里做错了?在今天来看你会做哪些不同的决策?对于工程师来说,回答这些问题是一种绝佳的提升软件设计能力和自我进步的方式。

4. 结论(Conclusions)

在软件项目中,围绕解决最难的问题,设计文档是一种获得清晰度以并达成共识的绝佳方法。设计文档可以节省金钱,因为在前期足够的调研可以帮助避免过早就进入编码细节却未能完成项目目标;设计文档又花费金钱,因为编写和审查文档消耗时间。因此,在你项目中明智地做出选择。

在考虑是否编写设计文档的时候,思考如下问题:

●你是否对正确软件设计不确信?在前期消耗时间来获取确定性是否合理?

●在设计阶段引入相关的资深工程师是否有帮助?他们可能没有时间审查所有代码。

●软件设计是否是模糊的,甚至是有争议的?因此围绕这一问题在组织层面达成共识会很有价值?

●我团队是否有时候会在设计中忘记考虑隐私、安全、日志或者其他横切关注点?

●在组织中是否非常需要遗留系统设计的文档?这样可以让大家在 high-level 快速了解系统。

如果对于上述的问题你有三个或更多“是”的回答,那么在你开始下一个软件项目的时候,设计文档大概率是个不错的方法。

文章目录
  1. 1. 1. 设计文档的构成
    1. 1.1. 1.1 上下文和范围(Context and scope)
    2. 1.2. 1.2 目标和非目标(Goals and non-goals)
    3. 1.3. 1.3 实际设计(The actual design)
      1. 1.3.1. 1.3.1 系统上下文图(System-context-diagram)
      2. 1.3.2. 1.3.2 APIs
      3. 1.3.3. 1.3.3 数据存储(Data storage)
      4. 1.3.4. 1.3.4 代码和伪代码(Code and pseudo-code)
      5. 1.3.5. 1.3.5 约束条件(Degree of constraint)
    4. 1.4. 1.4 候选设计(Alternatives considered)
    5. 1.5. 1.5 横切关注点(Cross-cutting Concerns)
    6. 1.6. 1.6 设计文档的长度(The length of a design doc)
  2. 2. 2. 什么时候不要编写设计文档(When not to write a design doc)
  3. 3. 3. 设计文档的生命周期(The design doc lifecycle)
    1. 3.1. 3.1 创建和快速迭代(Creation and rapic iteration)
    2. 3.2. 3.2 评审(Review)
    3. 3.3. 3.3 实现和迭代(Implementation and iteration)
    4. 3.4. 3.4 维护和学习(Maintenance and learning)
  4. 4. 4. 结论(Conclusions)