设计不是为了炫技,而是为了控制复杂度。好的设计让变更变便宜,坏的设计让每一次需求都像重写。
本文整理软件设计中常见的方法、原则与实践,作为速查与索引。
1. 设计方法
1.1 Design by Contract 契约式设计
由 Bertrand Meyer 提出,核心是契约三要素:
- 前置条件 (Precondition):调用者必须满足的条件
- 后置条件 (Postcondition):被调用者保证的结果
- 不变式 (Invariant):调用前后始终为真的约束
把接口当成契约,违背契约就应快速失败(fail-fast),而不是默默容错。
适合用于核心领域模型、公共库/ SDK 的 API 设计。
1.2 Convention over Configuration 约定优于配置
通过合理的默认值与命名约定,减少不必要的配置。只有在约定不满足需求时,才提供配置覆盖。
典型例子:
- Rails / Spring Boot / Nuxt / Next.js 的目录即路由
- ESLint / Prettier 的零配置即可工作
- RESTful 资源的命名约定
收益:降低心智负担、提升一致性;代价:需要团队认同并遵守约定。
1.3 12-Factor App
为构建可移植、可扩展的 SaaS 应用提供的方法论。
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-end、low-code、mini。
3. 设计模式与工程实践
3.1 设计模式
模式是特定上下文下的通用解法,不是银弹。
系统学习推荐:
前端常用:单例、工厂、策略、观察者/发布订阅、装饰器、代理、适配器、责任链。
提示:先写出能工作的代码,再识别坏味道,最后再引入模式。不要为了模式而模式。
3.2 依赖注入 (DI)
DIP 的落地手段。将依赖从外部传入,而不是内部 new 出来。
// 不好:硬编码依赖,难以测试
class UserService {
private api = new AxiosApi()
}
// 好:依赖反转 + 注入
class UserService {
constructor(private api: Api) {}
}
// 使用时注入:new UserService(fetchApi) / new UserService(mockApi)
前端容器:React Context、InversifyJS、TSyringe,或最简单的函数参数注入。
3.3 类与组合的选择
- 需要状态 + 行为 + 继承且层次稳定时,类合适
- UI 复用、逻辑复用、跨框架复用时,优先组合:
hooks + 函数 + 对象组合
4. 小结
| 维度 | 关键词 | 一句话 |
|---|---|---|
| 方法 | 契约、约定、12-Factor | 用契约保证正确性,用约定降低配置,用 12-Factor 保证工程可交付 |
| 原则 | SOLID、DRY、ETC、SoC、LoD、组合 | 让模块职责单一、易于扩展、依赖抽象、知识最少、复用靠组合 |
| 实践 | 模式、DI、类与组合 | 模式按需引入,依赖通过注入解耦,优先组合 |
做好设计的检验标准只有一条:下一次需求来临时,改动是否更小、风险是否更可控。
5. 相关阅读(站内)
本篇为总纲,细节与案例见以下子文,已做双向链接:
| 主题 | 文章 | 要点 |
|---|---|---|
| 原则 | software-development-principles | YAGNI / KISS / DRY 的取舍与三次法则,前端“不要提前封装/不要提前微前端”案例 |
| 架构 | architecture | CQRS 读写分离模式 |
| 组件 | 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-valtio | zustand / jotai / valtio 三种状态模型对比(中心化/原子化/Proxy) |
| 实现 | ts | TS 装饰器 + design:type 元数据实现 DI 容器 |