设计不是为了炫技,而是为了控制复杂度。好的设计让变更变便宜,坏的设计让每一次需求都像重写。

本文整理软件设计中常见的方法、原则与实践,作为速查与索引。

1. 设计方法

1.1 Design by Contract 契约式设计

由 Bertrand Meyer 提出,核心是契约三要素

  • 前置条件 (Precondition):调用者必须满足的条件
  • 后置条件 (Postcondition):被调用者保证的结果
  • 不变式 (Invariant):调用前后始终为真的约束

把接口当成契约,违背契约就应快速失败(fail-fast),而不是默默容错。

适合用于核心领域模型、公共库/ SDK 的 API 设计。

1.2 Convention over Configuration 约定优于配置

https://en.wikipedia.org/wiki/Convention_over_configuration

通过合理的默认值与命名约定,减少不必要的配置。只有在约定不满足需求时,才提供配置覆盖。

典型例子:

  • Rails / Spring Boot / Nuxt / Next.js 的目录即路由
  • ESLint / Prettier 的零配置即可工作
  • RESTful 资源的命名约定

收益:降低心智负担、提升一致性;代价:需要团队认同并遵守约定。

1.3 12-Factor App

为构建可移植、可扩展的 SaaS 应用提供的方法论。

https://12factor.net/zh_cn/

12 要素速览:

  1. 基准代码:一份基准代码,多份部署
  2. 依赖:显式声明并隔离
  3. 配置:在环境中存储配置
  4. 后端服务:把后端服务当作附加资源
  5. 构建、发布、运行:严格分离
  6. 进程:以一个或多个无状态进程运行
  7. 端口绑定:通过端口绑定提供服务
  8. 并发性:通过进程模型进行扩展
  9. 易处理:快速启动和优雅终止
  10. 开发/生产环境等价:尽可能保持一致
  11. 日志:把日志当作事件流
  12. 管理进程:把管理任务当作一次性进程

前端/ Node 实践对照:.env 管理配置、无状态服务、日志 stdout、CI 构建产物不可变等。

架构维度延伸:同属架构方法论的还有 CQRS(读写分离),见 architecture

2. 设计原则

2.1 SOLID

单一职责、开闭、里氏替换、接口隔离、依赖反转

1. SRP 单一职责原则

一个类/模块/函数应该只有一个变更的理由。

  • 每一个单元只做好一件事
  • 前端体现:一个组件只负责 UI 展示或只负责数据获取,不要混在一起;一个 hook 只封装一种逻辑

2. OCP 开闭原则

对扩展开放,对修改关闭。

  • 通过接口、抽象类、策略模式、插件机制来扩展,而不是直接改旧代码
  • 前端体现:表单校验器用 validator 策略扩展,而不是在 if/else 里不断加分支

3. LSP 里氏替换原则

超类对象可以被子类对象替换而不破坏正确性。

  • 子类必须完全实现父类的契约,不能收窄前置条件或放宽后置条件
  • 违反示例:子类重写方法直接 throw Error('not support')

4. ISP 接口隔离原则

许多特定于客户端的接口比一个通用接口更好。

  • 保持接口小、专注、具体,避免“胖接口”
  • 前端体现:UserCardProps 不要继承一个包含 20 个字段的 User,只传需要的 name/avatar

5. DIP 依赖反转原则

高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节。

  • 通过依赖注入 (DI) 解耦
  • 前端体现:组件依赖 fetcher: () => Promise<Data> 抽象,而不是直接 import axios;便于测试与替换
  • TS 落地:Reflect.getMetadata('design:type') + 容器实现自动注入,见 ts 对装饰器 DI 的示例

2.2 其他核心原则

DRY - Don’t Repeat Yourself

不要重复自己。但不要为 DRY 而 DRY:过早抽象比重复更昂贵。先容忍重复,等到出现第三次时再抽象。详见 software-development-principles 对 DRY 与 YAGNI 冲突的讨论。

ETC - Easier To Change

来自《程序员修炼之道》,比 DRY 更本质:ETC 是目标,DRY 只是手段。

每次设计决策都问:这样做是否让未来变更更简单?

YAGNI / KISS - 你不会需要它 / 保持简单

  • YAGNI (You Aren’t Gonna Need It):不要为未来可能的需求提前编码,只实现当前真正需要的功能。把复杂度推迟到真正需要时再引入,详见 software-development-principles
  • KISS (Keep It Simple, Stupid):解决方案本身要简单。YAGNI 关注“不要做不需要的”,KISS 关注“不要把需要的做复杂”。

经验:遵循三次法则(Rule of Three),重复出现三次再抽象,避免为 DRY 而过度设计。

关注点分离 (Separation of Concerns)

将不同关注点拆到不同模块:UI / 状态 / 数据获取 / 业务规则分离。

前端常见分层:components(展示) / hooks(逻辑复用) / services/api(数据) / store(状态)

状态分层实例:中心化 vs 原子化 vs Proxy 的取舍见 zustand-vs-jotai-vs-valtio

最少知识原则 / 迪米特法则 (Law of Demeter)

一个对象应该对其他对象有最少的了解。

  • 不要链式调用 a.getB().getC().doSomething()
  • 只与直接朋友通信,通过封装暴露意图明确的方法:a.doSomething() 内部再去协调 B、C

收益:降低耦合,减少“牵一发而动全身”。

组合优于继承 (Composition over Inheritance)

优先用组合来复用,而不是继承。

  • 继承是白盒复用(强耦合父类实现),组合是黑盒复用(只依赖接口)
  • React 已验证:HOC / render props / hooks 组合 > class extends

延伸阅读:类与组合

https://medium.com/@dan_abramov/how-to-use-classes-and-sleep-at-night-9af8de78ccb4

Dan Abramov 指出:用类容易陷入继承层次,而用函数+闭包+组合更灵活,也更符合 JS 心智。

组件层实践:无头组件与容器/展示组件分离见 component-design;微前端与低代码的架构取舍见 micro-front-endlow-codemini

3. 设计模式与工程实践

3.1 设计模式

模式是特定上下文下的通用解法,不是银弹。

系统学习推荐:

https://javascriptpatterns.vercel.app/patterns

前端常用:单例、工厂、策略、观察者/发布订阅、装饰器、代理、适配器、责任链。

提示:先写出能工作的代码,再识别坏味道,最后再引入模式。不要为了模式而模式。

3.2 依赖注入 (DI)

DIP 的落地手段。将依赖从外部传入,而不是内部 new 出来。

// 不好:硬编码依赖,难以测试
class UserService {
  private api = new AxiosApi()
}

// 好:依赖反转 + 注入
class UserService {
  constructor(private api: Api) {}
}
// 使用时注入:new UserService(fetchApi) / new UserService(mockApi)

前端容器:React ContextInversifyJSTSyringe,或最简单的函数参数注入。

3.3 类与组合的选择

  • 需要状态 + 行为 + 继承且层次稳定时,类合适
  • UI 复用、逻辑复用、跨框架复用时,优先组合:hooks + 函数 + 对象组合

4. 小结

维度关键词一句话
方法契约、约定、12-Factor用契约保证正确性,用约定降低配置,用 12-Factor 保证工程可交付
原则SOLID、DRY、ETC、SoC、LoD、组合让模块职责单一、易于扩展、依赖抽象、知识最少、复用靠组合
实践模式、DI、类与组合模式按需引入,依赖通过注入解耦,优先组合

做好设计的检验标准只有一条:下一次需求来临时,改动是否更小、风险是否更可控

5. 相关阅读(站内)

本篇为总纲,细节与案例见以下子文,已做双向链接:

主题文章要点
原则software-development-principlesYAGNI / KISS / DRY 的取舍与三次法则,前端“不要提前封装/不要提前微前端”案例
架构architectureCQRS 读写分离模式
组件component-design无头组件(Headless UI)、Presentational and Container Components
架构micro-front-end微前端特点、Why Not Iframe、Module Federation 实现思路
架构low-code低代码 DSL、画布、formily/nocobase、协议驱动
架构mini小程序跨端设计原则(抽象与统一、最大化复用、渐进增强)与方案选型
状态zustand-vs-jotai-vs-valtiozustand / jotai / valtio 三种状态模型对比(中心化/原子化/Proxy)
实现tsTS 装饰器 + design:type 元数据实现 DI 容器

参考