系统设计原则Skill system-design

这个技能教授如何构建可重用和可维护的软件系统,通过管理复杂性、设计深模块、隐藏信息等原则。适用于软件架构师、开发人员和系统设计师,帮助创建高效、易于维护的代码。关键词:系统设计,软件架构,模块化,复杂性管理,代码重用,信息隐藏,战略编程。

架构设计 0 次安装 0 次浏览 更新于 3/19/2026

name: 系统设计 description: 构建可重用编码系统的原则。适用于设计模块、API、CLI 或其他供他人使用的代码。基于 John Ousterhout 的 “A Philosophy of Software Design”。涵盖深模块、复杂性管理和设计红色标志。 tags:

  • 设计
  • 架构
  • 模块
  • 复杂性

系统设计

构建可重用、可维护编码系统的原则。来自 John Ousterhout 的 “A Philosophy of Software Design”。

核心原则:对抗复杂性

复杂性是大多数软件问题的根本原因。它逐步积累——每个捷径都增加一点,直到系统变得不可维护。

复杂性定义: 任何使软件难以理解或修改的东西。

症状:

  • 变更放大:简单变更需要多次修改
  • 认知负载:进行变更所需了解的内容量
  • 未知未知:不清楚需要改变什么

深模块

最重要的设计原则:使模块深

┌─────────────────────────────┐
│      简单接口      │  ← 小表面积
├─────────────────────────────┤
│                             │
│                             │
│    深实现      │  ← 大量功能
│                             │
│                             │
└─────────────────────────────┘

深模块: 简单接口,背后隐藏大量功能。

浅模块: 接口复杂度相对于提供的功能高。红色标志。

示例

深: Unix 文件 I/O - 仅 5 个调用(open, read, write, lseek, close)隐藏了巨大的复杂性(缓冲、缓存、设备驱动程序、权限、日志)。

浅: Java 的文件读取需要 BufferedReader 包装 FileReader 包装 FileInputStream。接口复杂度与实现复杂度匹配。

应用此原则

  • 偏好做更多事情的较少方法,而不是许多小方法
  • 积极隐藏实现细节
  • 模块的接口应比其实现简单得多
  • 如果接口与实现一样复杂,重新考虑抽象

战略与战术编程

战术: 现在让它工作。每个任务增加小的复杂性。债务累积。

战略: 投资时间在良好设计上。初始较慢,长期更快。

进度
   │
   │        战略 ────────────────→
   │       /
   │      /
   │     / 战术 ─────────→
   │    /                    ↘ (变慢)
   │   /
   └──┴─────────────────────────────────→ 时间

经验法则: 将开发时间的 10-20% 用于设计改进。

工作代码不够

“工作代码”不是目标。目标是既工作又有良好设计。如果你满足于“它能工作”,你就是在进行战术编程。

信息隐藏

每个模块应封装其他模块不需要的知识。

信息泄漏(红色标志): 相同知识出现在多个地方。如果一处改变,所有都必须改变。

时间分解(红色标志): 根据事情发生的时间而不是它们使用的信息来拆分代码。经常导致泄漏。

应用此原则

  • 问:“这个模块封装了什么知识?”
  • 如果答案是“不多”,模块可能很浅
  • 根据所知而不是运行时间分组代码
  • 默认私有;只暴露必要的内容

通过设计消除错误

异常增加复杂性。处理它们的最佳方式:设计使其不可能发生。

而不是:

function deleteFile(path: string): void {
  if (!exists(path)) throw new FileNotFoundError();
  // 删除...
}

做:

function deleteFile(path: string): void {
  // 直接删除。如果不存在,目标已达到。
  // 无需处理错误。
}

应用此原则

  • 重新定义语义使错误变得无关紧要
  • 内部处理边缘情况而不是暴露它们
  • 更少的异常 = 更简单的接口 = 更深的模块
  • 问:“我能改变定义使这不是错误吗?”

通用模块

某种程度上通用的模块比特殊用途的模块更深。

不要太通用: 不要在你需要一个函数时构建框架。

不要太具体: 不要硬编码限制重用的假设。

最佳点: 以自然处理明天问题的方式解决今天的问题。

要问的问题

  1. 覆盖所有当前需求的最简单接口是什么?
  2. 这个方法将用在多少情况下?
  3. 这个 API 对我的当前需求易于使用吗?

将复杂性向下推

当复杂性不可避免时,将其放在实现中,而不是接口中。

坏: 将复杂性暴露给所有调用者。 好: 内部处理复杂性一次。

模块有简单接口比简单实现更重要。

示例

配置:不是要求调用者配置所有内容,而是提供合理的默认值。内部处理选择默认值的复杂性。

设计两次

在实现之前,考虑至少两种不同的设计。比较它们。

好处:

  • 揭示你没有意识到的假设
  • 通常第二种设计更好
  • 即使第一种设计胜出,你理解了原因

不要跳过: “我想不出其他方法”通常意味着你没有足够努力。

红色标志摘要

红色标志 症状
浅模块 接口复杂度 ≈ 实现复杂度
信息泄漏 相同知识在多个模块中
时间分解 代码按时间拆分,而不是信息
过度暴露 接口中太多方法/参数
传递方法 方法除了调用另一个外很少做其他事
重复 相同代码模式多次出现
特殊-通用混合 通用代码与特殊用途代码混合
联合方法 不能理解一个而不读另一个
注释重复代码 注释说明代码明显做什么
模糊名称 名称没有传达太多信息

应用于 CLI/工具设计

当构建 CLIs、插件或工具时:

  1. 深命令: 做很多事情的较少命令,而不是许多浅命令
  2. 合理默认值: 在常见情况下无需配置工作
  3. 渐进披露: 简单用法优先,高级选项可用
  4. 一致接口: 所有命令使用相同模式
  5. 错误消除: 设计使常见错误不可能发生

示例:良好 CLI 设计

# 深:一个命令处理好常见情况
swarm setup

# 不浅:不需要 10 个标志用于基本使用
# 合理默认值:选择合理模型
# 渐进:高级用户以后可以自定义

关键要点

  1. 复杂性是敌人。 每个设计决策都应减少它。
  2. 深模块胜出。 简单接口,丰富功能。
  3. 隐藏信息。 每个模块拥有特定知识。
  4. 设计消除错误。 改变语义以消除边缘情况。
  5. 设计两次。 总是考虑替代方案。
  6. 战略 > 战术。 投资于设计,而不仅仅是工作代码。
  7. 将复杂性向下推。 实现吸收复杂性,接口保持简单。