日拱一卒,功不唐捐
软件设计的哲学

目录

这本书就是关于如何使用复杂性来指导软件设计的整个生命周期。

日期 学习内容 主题 印象最深刻的金句/观点/知识点等 你有哪些思考/启发/收获等
复杂性的本质 什么是复杂性 复杂性来自于依赖性和模糊性的积累。随着复杂性的增加,它会导致变更放大、高认知负荷和未知的未知。 1. 复杂三问:
认知负荷
变更放大
未知的未知

1. 这个需求为什么要改这么多地方?
2. 为了正确使用它,我必须提前知道什么?
3. 这些必要知识,我能从接口和文档中发现吗?
怎么应对复杂性 应对复杂性的方法 1. 第一种方法是通过使代码更简单和更易理解来消除复杂性。例如,可以通过消除特殊情况或以一致的方式使用标识符来降低复杂性。
2. 解决复杂性的第二种方法是封装它,以便程序员可以在系统上工作而不会立即暴露在所有复杂性的面前。这种方法称为模块化设计
1. 消除复杂性:简化实现;
2. 封装复杂性:将必要的协调工作集中在模块内部,让其他部分通过接口使用它;
3. 核心思想,简化信息,集中信息。
能工作的代码是不够的 战略式编程 好的设计不是免费的。它必须是你持续投资的东西,这样小问题才不会累积成大问题。幸运的是,好的设计最终会收回成本,而且比你想象的要早。 方案评审、阅读优秀架构书籍的目的,就是让设计更简单,不要引入不必要的复杂性。
模块化设计 为什么需要模块化设计? 通过将模块的接口与其实现分开,我们可以将实现的复杂性对系统的其余部分隐藏起来。模块的用户只需要了解模块接口提供的抽象。 1. 系统设计模块化;
2. 好的模块,
a.考虑到未来技术发展;
b.引用负担很小;
深模块 什么是深模块? 最好的模块是那些提供强大功能但具有简单接口的模块。 1. 深模块:接口简单、封装功能更多;
2. 浅模块:接口较提供的功能较为复杂;
怎么设计深模块 信息隐藏和信息泄露 信息隐藏和深模块密切相关。如果模块隐藏了很多信息,则往往会增加模块提供的功能,同时还会减少其对外接口数量。这使得模块更深。相反,如果一个模块没有隐藏太多信息,则它要么功能不多,要么接口复杂。无论哪种方式,模块都是浅的。 1. 信息泄露:多个模块依赖一个规则,如果规则更改,多个模块需要一起更改;(尽量某个规则只存在一个类中)
2. 信息隐藏:隐藏不需要的细节,比如tcp包隐藏拥塞控制等细节;
3. 返回的应该尽可能简单;如果返回的是单个结构体,好于返回多个值;
通用的模块是更深的 通用vs专用 不管是专用的类或方法还是代码里的特殊情况,都是软件复杂性的主要来源。专用代码无法完全消除,但通过好的设计能够显著减少专用代码,并将专用代码与通用代码分开。这能使类更深、做到更好的信息隐藏以及让代码更简单、更清晰。 1. 对于涉及到多个模块的功能,尽可能这个功能按照模块拆分,每个模块做自己的通用API;
2. linux驱动,专用下移;
3. i/o api,专用上移;
4. 文本编辑中,撤销功能不应该属于文本类,而是单独的类别;
5. 一定要合理分类。
不同的层级,不同的抽象 透传方法,透传对象 “不同的层级,不同的抽象”规则只是一种思想的应用:如果不同的层级具有相同的抽象,例如透传方法或装饰器,则很有可能它们没有提供足够的收益来弥补它们所增加的元素。 1. 很有用的一个章节:
2. 现在软件设计都是基于分层,每一层有自己的函数,如果上游直接调用下游函数而不加任何修饰,那这个函数(方法)就是透传方法,应该看看是否可以优化该方法,需要去看这个方法是不是“提供了独特的价值”;(A→A’→B)→(A→B)
3. 装饰器模式是有必要的,这个调用了下层方法但是还提供了额外的功能,比如BufferdIOStream和IOStream;
4. 透传对象,这里考虑有没有优化,如果多层对象,某个参数,只在最低级用过,那么这个参数是否可以封装到context里,全局变量里,共享对象里。
下沉复杂性 复杂性留给自己 在开发模块时,为了减少用户的痛苦,要找机会给自己多吃一点苦。 1. 有时候不知道参数选什么是正常的,比如失败重试次数、失败处理策略、健康检查间隔;
2. 这时候可以选择把这个复杂性留给自己,都能提高使用者的体感,(比如自动调整的健康检查配置,或者单纯的默认值也是)
3. 原则:在模块职责范围内,替上游承担复杂工作,同时保留上游需要作出的业务决定。
在一起更好还是分开更好? 拆分还是组合模块? 在决定是组合还是分开时,目标是降低整个系统的复杂性并改善其模块化。

拆分或组合模块的决定应基于复杂性。请选择一种可以提供最好的信息隐藏、最少的依赖关系和最深的接口的结构。
1. 组合到一起:
信息共享:读取和解析放一起;
简化接口:FileInputStream 和 BufferedInputStream组合到一起,降低认知负担;
组合消除重复:警惕重复出现;
2. 通用和专用分离:一个模块围绕一种通用机制组织;针对具体用途的规则,放在对应的业务模块中。(区分业务和infra);
3. 拆分和组合方法:原则是“高内聚,低耦合”,每个类,每个方法一定尽可能的清晰;
何时拆分,何时组合? 拆分还是组合模块 拆分或组合,都以降低整体复杂性为目标。拆分后应能独立理解,组合后应能减少依赖或简化使用。 1. 对于拆分,问:
新函数是否有清楚、简单的输入输出?阅读调用者时,能否不看新函数的实现?
阅读新函数时,能否不依赖调用者的内部细节?
2. 对于组合,问:
是否减少了调用步骤、中间状态或重复规则?
是否让原本需要来回对照的代码更容易理解?
通过定义来规避错误 异常处理 本章主要的教训是应该减少必须处理异常的地方的数量。在一些情况下,可以修改操作的语义,以便正常行为可以处理所有情况,从而无需报告任何异常情况 1. 处理方式两种:尽管有错误但仍然向前推进;终止并向上报告;
2. 异常的复杂性来自于异常处理的代码:总的来说就是优先用API定义减少可能出现的异常(比如数组截取[start, end],允许start,end为任何值并做定义可以消除相关参数异常的抛出),然后如果真的出现异常,尝试是否可以屏蔽(包括重试等方法内部消化,当然这里重试次数有上限的,如果达到上限就不得不抛出,这时候考虑后续处理;)或者可以聚和(把异常处理放到一起),崩溃是指遇到这个异常最好直接崩溃(比如malloc失败都要判null,不如封装一个函数,内部调malloc失败然后退出);
崩溃恢复要考虑一致性问题:如果真的恢复之前的状态怎么恢复(挨个回滚已经操作了的部分,或者有效性检查先进行,准备一份副本,让后再最后采用类似RCU替换的方式)
设计两次 设计软件原则 设计两次的方法不仅可以改善你的设计,而且可以提高你的设计能力。设计和比较多种方法的过程将教会你使设计变得更好或更差的因素。这将使你更容易排除不好的设计,并钻研真正伟大的设计。 在方案评审的时候提供多个备选,打开自己的思路,做不同的trade-off;这几个方案应该有足够的差异化。
不写注释的四个借口 注释:减少认知负荷和未知的未知 良好的注释不是失败。它们增加了代码的价值,并发挥着定义抽象和管理系统复杂性的基本作用 多写好的注释:注释和文档应保存设计意图、使用约定与重要限制,让开发者不用重新推导实现,也能正确使用和修改系统;这些说明应随代码一起维护。
注释应该描述代码中难以理解的内容 怎么写好的注释? 开发人员无需阅读除其外部可见声明以外的任何代码就应该能够理解模块提供的抽象。 1. 注释不应重复代码;
2. 注释提供的是代码没有的信息,可以是更高层的实现目的,可以是更底层的说明这个变量实际含义;
3. 接口文档是一些抽象;实现的文档表明为什么这么做;
选取名称 代码变量、函数命名 精心选取的名称能让代码更容易理解。当有人第一次遇到该变量时,他们对行为的第一次猜测就是正确的,而不需要太多的思考。 在review kpd代码的时候,发现当前代码很多变量/函数名称不合理,应该提前改掉。
这个名称是否能够反映出,他的实际意图?
先写注释 把注释变成设计过程的一部分 编写注释的最佳时间是在过程开始时。首先编写注释使文档成为设计过程的一部分。这不仅可以产生更好的文档,还可以产生更好的设计,并使编写文档的过程更加愉快。 1. 注释实际上是实现的抽象,是文档的一部分,如果实现目标足够清楚,这个注释应该很好写;
2. 先把这个注释理清楚,后续的实现过程中边理注释边理实现才更好。
3. 注释清楚不清楚是设计合理不合理的信号;
修改现有代码 注释很重要 如果注释比代码更高层级和更抽象,则注释更易于维护 先写清接口的职责、行为和约束,再实现它,并在开发中同步修正注释
一致性 统一规范 如果系统是一致的,则意味着相似的事情以相似的方式完成,而不同的事情则以不同的方式完成。 1. 名称/编码风格/接口/设计模式/不变量,保持一致;
2. 一致性的优点就是降低认知负担,减少系统复杂度;
代码应该是易理解的 怎么写出清晰的代码 当系统的重要信息对于新开发人员而言并不明显时,就会产生模糊性。解决模糊性的方法是以容易理解的方式编写代码。本章讨论了一些使代码更易理解或更难理解的因素。 1. 软件应被设计成易于阅读的而不是易于编写的;
2. 好的抽象、充足的信息、名称/注释;
软件发展趋势 不同类开发的初衷都是为了做抽象,减少复杂度 每当你遇到有关软件开发范式的新提案时,就必须从复杂性的角度对其进行考察:该提案确实有助于最大程度地降低大型软件系统的复杂性吗? 1. 面向对象编程和继承;敏捷开发(战术式);单元测试;(问题越早发现越好)测试驱动开发;(战术式)设计模式;(应用/识别出设计模式)
2. 用是否减少理解和修改成本来评价开发方法
性能设计 如何考虑性能 本章总体上最重要的经验是,简洁的设计和高性能是可以兼容的。 1. 区分旁路和关键路径,关键路径应该尽可能的短;
2. 性能测试确定瓶颈后再写代码优化性能;
决定什么是重要的 有重点 良好软件设计中最重要的元素之一是区分什么是重要的和什么是不重要的。围绕重要的事情设计软件系统。对于不重要的事情,尽量减少它们对系统其余部分的影响。重要的事情应该得到强调以使其更明显,不重要的事情应该尽可能隐藏起来。 识别并突出真正重要的概念,围绕它们设计系统;通过封装和简化,让使用者需要关心的事情尽量少
  1. 复杂性?
    1. 认知负荷;
    2. 改动放大;
    3. 未知的未知;
  2. 减少复杂性?
    1. 信息简化;(一致性表示、去除额外信息)
    2. 信息集中;(模块化设计)
  3. 透传方法?
    1. 透传方法通常就是调用低层级的类,不提供新的API;
    2. 透传方法的出现,代表类的职责划分不清晰。
    3. 优化方式:(A→A’→B)→(A→B)(信息集中)
  4. 透传对象?
    1. 在不同函数,不同模块都需要一个对象时,怎么传递:
      1. 直接传对象;
      2. 全局共享对象;
      3. 设置一个context,传到context里;✅(信息集中)
  5. 异常处理?
    1. 用API定义异常;(比如dataplane的api的异常处理只用限制dataplane正常运行,其他限制放到control plane)(信息简化)
    2. 屏蔽;(通过重试来消化)(信息简化)
    3. 聚合;(看到多个,log_err(”aaa”),看是否可以抽象成一个函数)(信息集中)
  6. 异常处理方式?
    1. 重试;
    2. 崩溃;(谨慎考虑,如果不崩溃,会不会出现严重问题?)
    3. 向上报告+异常恢复;(上游对这个错误能处理吗?异常恢复一定要保证一致性。)
  7. 配置更新技巧?
    1. 配置更新技巧,就是先检查配置是否合法,然后合法之后再进行原子更新;
  8. 下沉复杂性?
    1. 在设计API时,要考虑所有输入(API:输入→输出),看看某些输入是不是必要的,直接去掉某个输入可以吗。(信息简化)
  9. 浅模块和深模块?
    1. 浅模块,就是只是加了一层封装没有做任何额外处理(A, A’);
    2. 深模块,封装了更多的处理;(A, ABC)(信息集中)
  10. 一致性?
    1. 在入职新团队、接收新的模块时,提前先了解团队规范,看代码也要检查这个规范是否满足和一致。(信息集中)
  11. 性能设计?
    1. 关键路径应该尽可能的短;(信息简化)
    2. 先测试再优化,识别瓶颈;
  12. 怎么写注释?(信息集中+信息简化)
    1. 抽象;
    2. 避免重复;
    3. 保存意图;(当成设计文档)

这里引用的内容是https://yingang.github.io/aposd2e-zh/

数据库学习笔记
Maglev 论文阅读笔记:高性能转发、一致性哈希与连接亲和性
© 2025–2026 Zhe-MingC
Powered by hexo | Theme is dewjo