Skip to content

软件设计原则与模式本质总览

系列:pattern/ · 原理与本质通识导读
更新日期:2026-07-08


这篇是什么

软件设计有两个核心维度:

  • 设计原则:回答「什么叫好设计」,提供判断标准与重构方向。
  • 设计模式:回答「怎么组织类与对象」,提供经过时间检验的具体落地结构。

设计模式的本质,是在「变化」与「稳定」之间划界,让系统对扩展开放、对修改关闭。 模式几乎总是设计原则的常见落地形状

建议的学习与实践顺序

高频模式建议优先:观察者、策略、工厂、组合、命令、装饰。它们覆盖了日常 80% 的「变化点」问题。其余模式在碰到具体问题时再查即可。


元原则:一切为了控制复杂度

几乎所有设计原则与模式最终都指向同一件事:降低理解系统、修改系统时的认知负担

症状含义常见对策
变更放大小改动牵动大片代码DRY、信息隐藏、深模块
认知负荷读代码要记太多上下文单一职责、显而易见、命名
未知未知改 A 不知道会坏 B正交、低耦合、测试

复杂度几乎总是增量堆上去的,没有「突然变烂」——见 软件设计的哲学 · 复杂度


一、软件设计原则全景

1. SOLID(面向对象五条)

面向对象模式背后最常引用的基石。详见 李建忠 · 02 面向对象设计原则

原则英文一句话违反时常表现
单一职责SRP一个模块只因一种原因而修改上帝类;改导出牵连 UI
开闭OCP对扩展开放,对修改关闭满屏 if (type == …)
里氏替换LSP子类可无损替换父类子类抛「不支持」
依赖倒置DIP依赖抽象,不依赖具体高层直接 new 具体类
接口隔离ISP接口小而专,不强迫无用方法实现类大量空方法

合成复用(CRP) 虽不在 SOLID 字母里,但与 SOLID 同等重要:优先组合(Has-A),慎用继承(Is-A)。策略、装饰、桥接的共同地基。

SOLID 之间的关系与变化点封装

SOLID 之上的核心操作:找到最可能变的那条轴,用抽象隔离开(即变化点管理)。

2. 经典原则(跨范式)

不限于 OOP,函数式、过程式同样适用。

原则英文核心注意
不要重复自己DRY每一条知识只在一处权威表达形似代码 ≠ 重复知识
保持简单KISS能简单就不复杂简单 ≠ 简陋
你不会需要它YAGNI无证据的变化别提前抽象避免过度抽象
迪米特法则LoD只与直接朋友交谈减少长链 a.b().c().d()
最少知识模块少假设邻居内部与信息隐藏同族
组合优于继承黑盒复用优于白盒复用继承税高,易破坏封装
针对接口编程调用方知契约,不知实现DIP 的日常说法
高内聚低耦合模块内紧、模块间松拆合的直觉目标

DRY 的精确定义

DRY 消灭的是知识重复,不是「两行长得像」。

  • 违反:连接失败提示、日志上传、防火墙预检各写一套分支与文案。
  • 可保留:两个循环结构相似,但表达不同的业务规则。 详见 程序员修炼之道 · 第 2 章

3. 务实原则(应对变化)

来自 程序员修炼之道,与 SOLID 互补:SOLID 偏结构,务实原则偏过程与心态

原则含义实践
正交性改 A 不应意外牵动 B模块边界清晰;少全局状态
可逆性无最终决定;保留回退延迟绑定;配置外置
解耦少假设;通过消息/接口协作事件、依赖注入
够好即可在约束下明确质量目标非降低专业标准
软件熵不维护必然腐烂每次改动让代码比来时干净一点
曳光弹端到端可运行薄骨架要演进,非一次性原型
早崩溃非法状态立即失败不传播脏数据
契约式设计前置/后置条件写清假设与断言分工
Rule of Three第三次重复再抽象平衡 YAGNI 与 DRY

优秀设计的试金石:优秀设计比烂设计更容易修改。若加功能总要改十处、怕碰旧代码,说明原则层面已欠债——见 设计模式的思考 · 过度设计

4. 模块与抽象原则(战略式编程)

来自 软件设计的哲学。与 SOLID 不冲突,但视角不同:SOLID 常从类出发;Ousterhout 从模块接口与认知负担出发。

原则含义反面
深模块接口简单、实现强大浅模块:接口仍复杂
信息隐藏决策藏在实现内信息泄漏:格式/协议/时序暴露给调用方
下拉复杂度难留给实现,简留给接口API 把本该内部处理的事推给调用方
不同层不同抽象相邻层语义可区分透传层:只转发不增值
适度通用略通用的模块往往更深过窄或过宽都变浅
Define errors out从 API 设计消灭非法状态到处 if (!valid)
Design it twice至少两套方案再定稿第一个想法直接写死
显而易见读者第一次读就懂聪明代码、嵌套回调链
战略式编程默认多投 10–20% 设计时间战术式:能跑就交差

深模块 vs 浅模块

classitis(为小而拆、接口泛滥)是浅模块的常见病因。拆合的唯一标准是复杂度增减,非 SOLID 字母游戏——见 第 9 章 · 拆还是合

5. 可读性与协作原则

设计不只关「模块形状」,也关人能否读懂、改得动

  • 命名揭示意图:清晰、一致、可搜索。
  • 注释写非显而易见之事:解释 why、假设与约束,而非重复代码在做什么。
  • 先写注释再写代码:作为梳理设计思路的一部分。
  • 一致性:跟随局部惯例,降低认知负荷。
  • 可测试性:难测往往意味着耦合太重,促使我们进行解耦。
  • 沟通:API、文档、评审都是设计的一部分。

6. 原则之间的关系与张力

原则会打架,工程就是在约束下取舍。

张力一侧另一侧怎么拿捏
YAGNI ↔ OCP别为假想需求抽象对扩展开放变化有证据再抽象;Rule of Three
DRY ↔ 显而易见抽函数减重复关键路径要一眼读懂事故路径可牺牲一点 DRY
SRP ↔ 深模块类要单一模块要「深」变更原因拆,非按行数拆
性能 ↔ 清晰先跑得快先读得懂先清晰,profile 后再优化热点
通用 ↔ 专用深模块可适度通用YAGNI三个真实用例前别大抽象

二、设计模式的本质

设计模式既不是库,也不是框架,更不是特定语言的语法糖。它是在反复出现的场景里,类与对象如何分工协作的命名经验。

1. 模式是「变化点管理术」

几乎所有模式都在回答同一个问题:哪一部分将来最可能变?怎样让「变」不影响「不变」? 读模式时,先问「这里的变化点是什么」,比背 UML 更有用。

模式隔离的变化点
策略可互换的算法
观察者谁会响应状态变化
工厂具体类型如何被创建
桥接抽象与实现两个维度
装饰动态叠加的职责
命令请求的发起与执行

2. 三条底层逻辑

GoF 23 种模式背后,反复出现三条主线:

2.1 依赖抽象,不依赖具体 (DIP)

高层模块不应绑死在 new FileLogger() 上,而应依赖 Logger 接口。换实现时,调用方不动——这是策略、工厂、桥接的共同地基。

2.2 组合优于继承 (CRP)

继承是白盒复用,子类与父类紧耦合。Has-A(持有一个策略、包一层装饰)是黑盒复用,往往更灵活。策略、装饰、桥接都在践行合成复用。

2.3 分离「做什么」与「谁来做 / 何时做」 (SRP)

分离什么典型模式
算法 vs 使用算法 the 上下文策略
请求 vs 执行者命令
通知 vs 响应观察者
构造 vs 使用工厂、建造者
树形结构 vs 遍历方式组合 + 迭代器

3. 模式是「角色关系」,不是固定类名

GoF 描述的是角色(如 Context、Strategy、ConcreteStrategy)。 真实代码里可以根据业务任意命名(如 CheckoutPaymentStrategyAlipay)——角色关系不变,命名随意

同一系统里常多个模式叠加:

  • 命令 + 备忘录 → 撤销栈
  • 组合 + 迭代器 → 遍历文档树
  • 工厂 + 单件 → 全局配置管理
  • 策略 + 模板方法 → 框架定流程、应用填步骤

模式的本质是可复用的微型架构片段

4. 三类模式:管三件不同的事

类型本质问题一句话
创建型对象怎么来new 从业务里挪走,隐藏构造复杂度
结构型类怎么拼用组合、包装、适配组织更大结构
行为型职责怎么分、怎么通信算法、状态、通知、请求如何流动

创建型管诞生,结构型管形状,行为型管动作与协作——这是对复杂度的三种切分。

5. 模式的代价:用复杂换灵活

每种模式都带来:更多的类与接口、更多的间接层、以及更高的理解成本。
因此模式的另一面是:有意识地接受一定复杂度,换取未来的可扩展性
简单问题用简单解法;模式不是勋章,是在确认真有变化压力时的投资

6. 模式 vs 框架 vs 语言

设计模式框架(Qt / VTK)现代 C++
是什么设计经验的名字已实现的大结构语言级抽象
例子策略、观察者信号槽、vtkInteractorStylestd::function、RAII
关系思想层模式已嵌入 API可简化实现,思想仍在
  • STL 迭代器 = 迭代器模式的标准化
  • std::function = 轻量策略 / 命令
  • Qt 信号槽 = 观察者思想的框架化
  • vtkCommand = Observer 回调载体(名字像 Command,语义是 Observer)

本质在思想;形态随语言与框架演进。


三、从坏味道到原则与模式(重构入口)

模式几乎总是重构的目标形状,而不是项目启动时的第一张架构图。在 CR 或重构时,可以通过「坏味道」反推需要采用的原则与模式。

1. 坏味道 → 对应原则与首要思考

坏味道可能违反的原则先想什么
满屏 if-else 按类型分支OCP变化点是否真是「算法/类型」
改一处坏十处正交、信息隐藏知识是否泄漏到多处
类几千行SRP、深模块有几种变更原因
继承树爆炸LSP、CRP能否改组合
调用方要懂内部格式信息隐藏能否下拉复杂度
接口巨大但实现空ISP、浅模块是否 classitis
复制粘贴同一段逻辑DRY是知识重复还是巧合
测试要起半个系统DIP、解耦能否注入抽象
注释只重复代码在做什么注释原则应写 why / 约束
单例满天飞可测试性、解耦是否真需要全局唯一

2. 坏味道 → 模式映射

坏味道建议模式
满屏 if-else策略 / 状态
类越来越大单一职责 / 门面
继承树爆炸桥接 / 组合 / 装饰
对象互相引用成网中介者 / 观察者
创建逻辑散落各处工厂 / 原型
撤销重做难做命令 + 备忘录

3. 原则 → 常见落地模式速查

原则 / 坏味道常落到的模式
OCP + 封装算法变化策略
OCP + 动态叠加职责装饰
DIP + 隐藏创建工厂
通知与响应分离观察者
请求与执行分离命令(Qt 撤销语义)
树形结构统一对待组合
子系统对外简单入口门面(ljz 14
抽象与实现两维变化桥接(ljz 07

4. Code Review 红旗 (避免落入陷阱)

  • 浅模块、接口膨胀、classitis
  • 信息泄漏(格式、协议、时序暴露)
  • 重复知识散落多处(真 DRY 问题)
  • 长依赖链、a.b().c().d()
  • 为未证实需求建抽象层(过度设计)
  • 继承过深或子类破坏父类契约(LSP)
  • 注释只复述代码、命名不可搜索
  • 难测、难 mock、单例绑死
  • 战术式补丁:只让当前用例过,系统整体复杂度更高

四、分层记忆:原则与模式地图

按「你在解决什么问题」选用,而非背清单。

若只记十句

  1. 复杂度是敌人,症状是变更放大、认知负荷、未知未知。
  2. 深模块:接口简单,实现强大。
  3. DRY 的是知识,不是形似代码。
  4. 正交:改一处不应意外牵动另一处。
  5. 依赖抽象(DIP),组合优于继承(CRP)。
  6. 封装变化点,有证据再为扩展投资(YAGNI ↔ OCP)。
  7. 信息隐藏:别把内部决策泄漏进 API。
  8. 显而易见 比聪明更重要。
  9. 优秀设计更容易修改——这是优秀设计的终极试金石。
  10. 原则与模式不是 KPI;可读、可测、可改才是。

重点与注意

重点先原则,再模式。模式名是手段,控制复杂度是目的。从坏味道出发重构,比一上来「模式驱动设计」更务实。
重点DRY + 正交 + 信息隐藏 + 深模块 四条线覆盖多数日常重构。
重点:模式 = 变化点隔离 + 依赖抽象 + 组合复用 的角色关系命名结构,可叠加使用。
注意:原则会冲突;用「变化是否有证据」「认知负担是否下降」作仲裁。
注意:违反原则不等于必须立刻上模式——有时一个朴素的、单一职责的函数就够了。
注意:框架里已内置大量原则与模式的落地;读 API 也是在读模式与原则的实践。


延伸阅读

基于 VitePress 强力驱动 | 记录技术与生活