软件设计哲学:降低复杂度的开发规范
在完成用户需求的同时,让开发者理解一次操作时需要知道的事情更少,让一个设计决策的变化影响更小。用代码和调用方的实际变化证明设计收益,不能用类数量、层数、行数或模式名称代替判断。
本规范是对 John Ousterhout《A Philosophy of Software Design》的工程化应用,不是书籍摘录。它归入后端开发规范,也适用于其他需要划分模块边界的工程任务。正文给出工作规则;需要判断边界或解释取舍时,阅读 设计示例与反例。原文入口见 来源与适用说明。
按任务使用
- 开发或修复:检查相关实现和调用方,直接实施范围内的设计改进与必要验证。
- 设计讨论:给出接口、职责和取舍,不擅自实施。
- 代码审查:用文件位置、具体场景和影响说明问题,不把每条原则变成必报问题,也不擅自改代码。
- 小改动:如果不改变抽象边界,沿用已有设计即可,不要求生成设计文档或比较多套方案。
用户的目标和项目约束优先。战略性编程不授权重写整个系统、破坏现有契约或扩大任务范围。
开始开发前:找到复杂度的来源
先读取项目约定、相关代码、调用链和测试,找出此次需求触及的规则与状态。重点观察:
- 变更放大(Change Amplification):一个规则改变时,哪些位置必须一起修改?
- 认知负荷(Cognitive Load):调用者要知道哪些参数组合、内部格式、调用顺序或特殊情况?
- 未知的未知(Unknown Unknowns):哪些必要条件或联动修改点没有明确入口可供发现?
继续追查背后的依赖与晦涩之处。优先减少常见开发和使用路径上的负担;不要为了简化一处罕用实现,反而复杂化所有调用方。
对影响边界的改动,先用几句话说明:模块向外承诺什么、在内部隐藏什么、由谁拥有相关状态。只记录与当前决策有关的信息,不强制为每个任务填写表格。
核心设计规则
1. 战略性编程(Strategic Programming)
功能正确是完成条件的一部分,同时检查本次实现是否给后续维护增加了不必要的负担。遇到当前改动直接涉及的重复规则或泄漏的实现细节,优先在其归属处做小幅修正,避免继续给每个调用方打补丁。
对重要的接口、模块边界或核心实现,先“设计两次”(Design It Twice):勾勒至少两种有实质差异的方案,必要时包含保留或扩展现有抽象。无需写两套完整实现。比较调用负担、信息归属、实现成本和性能约束;发现两者都有问题时,用这些问题寻找更好的边界,不只给第一个想法换名字。
“改动小”应指解决问题所需的合理范围,不等于追求最少改动行数。问自己:如果一开始就知道这个需求,当前边界还合理吗?在现有限制内修好它,不留下本可避免的特殊分支。
如果用户明确要求应急修复,先在限定范围内恢复正确行为,说明留下的具体约束;不要借设计原则延误交付,也不要承诺未经授权的后续重构。
2. 深模块(Deep Modules)
让调用者通过小而清晰的接口获得完整能力。接口包括签名,也包括错误语义、状态约束、资源生命周期和调用顺序。
从常见调用场景检查设计:调用者能否只理解契约就正确使用?是否被迫配置罕用选项或拼装内部步骤?把属于模块的协调逻辑放回模块。
“深”不等于方法很长、类很大或层数很多。内部仍需保持可读的结构;互不相关的能力不应合成万能服务。只有减少理解负担的拆分才值得保留。
拆分出子方法后,父方法应能仅凭子方法契约被理解,子方法也不应依赖读者熟悉父方法内部。若必须反复跳转才能读懂,重新考虑拆分。两个能力共享知识、双向经常一起使用或合并后能省去调用步骤时,考虑合并;仅仅单向依赖不足以合并,例如业务服务使用通用集合。
3. 信息隐藏(Information Hiding)
按知识和设计决策划分职责:格式解析、存储结构、缓存规则等细节,应有明确的所有者。检查更换内部表示时是否迫使无关调用者修改;若是,判断是否发生信息泄漏。
不要只按“先读取、再解析、最后保存”的时间顺序划分模块,导致多个模块共同理解同一文件格式。私有字段加公开 getter,也不自动构成信息隐藏。
隐藏实现,不隐藏调用者必须知道的业务结果、成本、失败条件或一致性保证。跨模块的真实依赖应由契约明确表达。
4. 不同层提供不同抽象
沿一个真实调用链,说明每层增加了什么语义或保证。例如,业务层表达领取资格,存储层表达记录的原子更新,驱动层表达数据库操作。
相同参数和同类操作在多层原样转发,是需要检查的信号。若一层没有承担契约转换或其他实际职责,考虑合并;若它承载项目要求的权限、事务、协议适配或兼容边界,保留并说清职责。不要机械删除短方法,也不要为“分层完整”新增空壳。
相同签名也可以承载有价值的分发、装饰或不同实现,判断实际职责而非外观。若参数只被最底层使用,却贯穿许多中间层,检查能否由现有对象或构造依赖持有;不要自动换成全局变量或万能 Context,制造隐式依赖。
5. 将复杂度放到能够负责的位置
属于一个模块的复杂性由它处理,让多个调用方共享简单用法;调用方的业务政策仍由调用方决定。不要把业务分支塞进通用基础组件。
新增配置项前,判断使用者是否确实比模块更有能力做这个决定。能依据已有信息计算合理值时,在模块内完成;确需开放配置时,优先提供有依据的常用默认值,同时明确可覆盖范围。不要用大量配置把未完成的设计推给使用者。
能通过契约消除特殊情况时,优先简化状态或操作语义。例如在业务允许的情况下提供幂等操作,让重复调用得到已定义的结果。不得靠吞异常、返回假成功或取消必要校验来制造简单接口。
6. 以抽象为开发增量
产品需求决定交付目标;实现时围绕需求所需的完整能力组织增量,每个增量包含清晰契约、实现、真实调用方和必要测试。
优先完善已有抽象,而不是为每个页面、按钮或渠道复制一份相似流程。设计当前确实需要的能力时,去掉无关的场景绑定,使接口适度通用;不必等待出现大量重复才设计清楚。
适度通用(Somewhat General-Purpose)的检查是:能否用更少的概念满足现有用途,同时不增加调用方的拼装工作?减少方法却增加大量模式参数,或只提供过细原语迫使调用方重复写循环,都不算改善。
这不要求每次新增类或接口,也不要求预建未来平台。一个正确修复了既有契约的改动同样是有效增量。只有当前需求或现有调用证据支持时,才引入额外扩展点。
7. 注释补充代码不能直接表达的知识
在新接口实现前,先尝试写清它的对外承诺。难以命名或需要解释大量例外时,回头检查抽象边界。
- 接口注释描述行为、单位、边界、副作用和失败语义,使调用者不必阅读实现。
- 实现注释解释设计理由、业务不变量、顺序约束和不明显的取舍。
- 不把逐行翻译当作解释;不把内部算法细节混入调用者不需要知道的接口说明。
- 即使某个契约可以从许多行代码推导出来,也可以用注释把它明确写出。
按项目约定使用中文注释和文档格式;项目明确要求方法注释时保证覆盖。修改行为时同步更新相邻注释,不能用文档掩盖错误实现。
设计理由放在维护者会看到的代码附近,不能只留在提交说明里。跨模块约束集中描述一次,在受影响位置引用;接口契约、实现理由和外部协议文档各有归属,避免多份解释逐渐失去一致性。
8. 命名、一致性与显然的行为
同一概念使用一致名称,不同概念明确区分;名称应暴露关键语义,例如业务时间与处理时间、字符位置与像素坐标。短小局部作用域可使用简短名字,跨越越远的名字越需要独立可理解。难以准确命名时,检查是否混合了多个概念。
相似问题遵循现有接口和错误约定,真实差异不要强塞进统一模式。返回多个有业务含义的值时,优先使用具名字段,避免让调用方猜元组位置或布尔值的含义。
对事件、回调、后台任务和隐式副作用,写清触发条件、执行上下文和生命周期。以首次阅读者的理解成本为准,不能用“读完代码就知道了”回应实质困惑。
9. 减少异常处理点,保留真实失败
先检查能否通过明确语义消除特殊情况;不能消除时,判断由哪里处理最合适:
- 局部恢复:模块确实能恢复并兑现契约时,在模块内处理,调用方不必重复恢复逻辑。
- 聚合处理:多个错误需要相同的请求终止、清理和响应流程时,在共同边界处理,保留必要的错误类别和诊断信息。
- 无法恢复:按项目的请求、任务或进程失败策略停止受影响操作。只有明确允许且符合运行模型时,才能终止整个进程。
异常是接口的一部分。恢复失败、资源清理、重试次数与幂等条件都要有实际依据;不引入无边界重试,不把书中的特定系统恢复方式照搬成默认策略。
10. 自然高效,优化以测量为依据
正常设计就避免明显重复的远程调用、I/O 和无必要的分配,但不为猜测的热点增加复杂结构。已有性能约束和测量证据时,可直接据此选择实现。
针对性能问题,先取得代表性负载下的基线并定位瓶颈,再考虑减少工作、改变数据结构或算法。必要时围绕常见关键路径整理实现,把罕见情况移出主路径,同时保留完整行为。
修改后用相同口径重测,并验证异常与边界行为。没有收益且仅增加复杂性的本次优化应撤回;不能用“更少行数”“更少层数”替代性能证据。
验证与完成
先验证需求与已有契约:根据变更选择现有测试、回归用例、编译或集成检查。修复缺陷时,优先让回归用例复现问题,再验证修复。测试应验证对外行为和重要边界,避免锁死私有方法和内部拆分。
设计与测试互相反馈。不要只围绕让一条条用例通过来堆补丁,也不要因为作者对 TDD 的批评而取消项目已有测试流程。抽象需要在真实调用与测试中一起完善。
再从调用方检查本次设计:
- 使用新能力还需要阅读模块内部吗?必要的契约是否写清?
- 一个规则的变化是否仍要在多个地方同步维护?
- 新增的层、参数和特殊情况是否有当前需求依据?
- 复杂性确实被封装了吗,还是仅换了文件位置或被推给调用者?
报告实际改动、设计收益和验证结果即可。设计收益用“调用方不再需要管理哪些步骤”“某规则集中到哪里”等事实表达,不声称测试通过证明了设计优秀。审查问题应包含具体证据、影响和最小可行建议;没有实质问题时直接说明。