软件设计:从复杂性到稳定性
软件设计的本质,是在变化的世界中建立可理解、可演化的秩序。它连接需求与解决方案,用模型对抗混乱,用结构控制复杂性。
一、设计的本质与目标
软件设计的核心工作,就是构建模型,用模型连接“问题空间(需求)”与“解空间(实现)”,并通过规范去约束实现,以在变化中保持系统的可控与稳定。
- **战术编程**:赶紧实现功能,能跑就行。
- **战略编程**:预先构建结构,控制复杂性。
软件设计就是战略编程的体现——它不只是写代码,而是构建“可长期生存的系统”。
设计原则是指导设计的经验总结,设计模式是问题导向的一系列方案或者设计思路,编码规范是实现可读性的约束手段。通过设计原则的指导,使用设计模式,经过编码规范约束并配合重构,保证代码的可读性、扩展性、可复用性,最终实现高内聚、低耦合的模块或系统
二、复杂性:设计要对抗的敌人
复杂性不是敌人本身,而是失控的结构。设计的意义,在于让复杂性有序地分布。
1. 复杂性的表现
- **变更放大**:看似简单的修改,却需要动很多地方。
- **认知负荷**:开发者要理解多少知识才能改动一行代码。
- **未知的未知**:不知道改哪里能实现目标。
2. 复杂性的根源
- **依赖过多**:难以管理的依赖关系。
- **语义模糊**:实体意义不明,职责混乱。
3. 降低复杂性的思路
在模块开发中要么对外暴露简单接口(方便用户),要么保持简单实现(方便维护)。抽象、分层、模块化、解耦、关注点分离,都是降低复杂性的手段。
三、关注点分离与信息隐藏
一切优秀的软件设计,都是在不断回答两个问题:“这部分应该知道什么?”、“这部分不该知道什么?”
1. 关注点分离(Separation of Concerns)
通过分解问题,让每个模块只关心自己的责任。这是一切设计原则的起点,可通过以下方式实现:
- 模块化与封装
- 单一职责
- 分层架构
- 清晰边界定义
2. 信息隐藏(Information Hiding)
每个模块应封装部分知识与决策,让这些细节只存在于内部实现中,而不出现在外部接口。这样模块间依赖的是抽象,而非细节,从而减少耦合、增强稳定性。
3. 接口与抽象
抽象是一种隔离机制。越抽象的接口代表模块越“深”,能隐藏更多复杂性。通过抽象与接口分离,可以让模块变得“可替换、可演化、可理解”。
四、设计原则:代码层的秩序
设计原则是从无序到有序的经验总结,是控制局部复杂性的策略。
1. SOLID 原则 —— 稳定代码结构的五根支柱
| 原则 | 含义 | 核心思想 |
|---|---|---|
| SRP 单一职责 | 每个模块只对一个行为负责 | 控制变更范围 |
| OCP 开闭原则 | 对扩展开放,对修改关闭 | 以抽象隔离变化 |
| LSP 里氏替换 | 子类可替代父类 | 保持行为一致性 |
| ISP 接口隔离 | 不依赖无关接口 | 控制依赖扩散 |
| DIP 依赖反转 | 依赖抽象而非实现 | 控制依赖方向 |
这五个原则共同形成一个目标:以抽象对抗变化,以接口维持秩序。
SRP:单一职责原则
任何一个软件模块都应只对某一类行为负责,修改一个类的原因应该只有一个
主要讨论的是函数与类的关系,当这个类需要做过多事情的时候,也就是出现了很多不相关的函数时,就需要分解这个类
OCP:开闭原则
设计良好的软件应该容易扩展,而禁止修改
- 将旧代码的修改量降低至最小,限制变化的范围
该原则要求在添加新功能时不需要修改代码。但是这条原则真的很容易做到吗?在繁杂的业务代码中,大部分情况下,业务发生变更,业务代码必须要进行修改。这就要求我们编写的代码可以适应未来的情况,可根据需求软编码的方式来变更业务逻辑。
依赖方向的控制: 通过接口来反转组件之间的依赖关系,使得高阶组件不会因低阶组件被修改而受到影响
信息隐藏:通过中间层使高层组件不过度依赖低层组件的内部细节
LSP:里氏替换原则
一个软件实体如果使用的是一个基类的话,那么它一定也可以使用其子类,而且它根本不能察觉出基类对象和子类对象的区别
animal.run();// ↓cat.run();如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度
- 是一个指导接口与其实现方式的设计原则
ISP:接口隔离原则
对模块来说,跟它无关的接口一旦发生变更,应该不能影响到该模块,不应该强迫客户依赖于它们不用的方法
使用多个专门的接口比使用单一的总接口要好
- 软件设计如果依赖了它并不需要的东西,会带来麻烦
DIP:依赖反转原则
高层模块不应该依赖于低层模块,二者都应该依赖于抽象抽象不应该依赖于细节,细节应该依赖于抽象。
当然某些情况下抽象必须依赖于细节,比如Object中对String的依赖
想要设计一个灵活的系统,则就应多引用抽象类型,而非具体实现。这么做的原因是接口相比实现更为稳定
主要关注的是系统中那些经常变动的
2. 平衡性原则 —— 从理性到节制
- **DRY(不要重复)**:重复是复杂性的温床。
- **YAGNI(你不会需要它)**:不要为未来的假设增加当下的负担。
- **Rule of Three(三次原则)**:在重复中发现抽象,在抽象中追求节制。
- **KISS(保持简单)**:设计的美感来自于“直觉一致、易于理解”。
- **POLA(最小惊奇原则)**:让系统行为符合常识,减少认知摩擦。
这些原则塑造了微观层面的“理性结构”,是代码世界的行为准则。
五、模块与层次:结构化复杂性的方式
1. 模块的深度与接口
模块的深度取决于其隐藏的知识量。深模块暴露简单接口,浅模块暴露细节。一个系统的深度结构,就是它抽象能力的体现。
2. 分层架构与接口传递
层与层之间的交互应最小化。“直通方法”虽然浅,但可以保持边界清晰。当参数传递变复杂时,可通过上下文对象降低复杂度。
3. 分开与合并的取舍
设计要回答的问题不是“拆还是合”,而是“哪里是知识的边界”。合并可减少重复,分离可隔离变化。关键在于信息共享、接口简化与依赖控制之间的平衡。
六、组件设计:系统层的秩序
当软件规模扩大到无法在一个脑中完全装下时,设计的重心从函数与类上升为组件与依赖图。
1. 组件的定义
组件是可独立部署与演化的最小单元。它既是技术边界,也是组织边界。良好的组件化是系统可维护、可扩展的前提。
2. 组件聚合原则(Cohesion)
| 原则 | 含义 |
|---|---|
| REP:复用/发布等同原则 | 复用与发布的最小单元应一致 |
| CCP:共同闭包原则 | 同时变更的模块应在同一组件中 |
| CRP:共同复用原则 | 经常一起被复用的类应在同一组件中 |
组件内高内聚,组件间低耦合。这些原则是 SRP 与 ISP 的组件级体现。
REP:复用/发布等同原则
组件中的模块和类之间应该有一个共同的主题或者大方向
CCP:共同闭包原则
应将那些会为了一个目的而同时修改的模块与类放到一个组件中
CCP 是 SRP 的组件版
CRP:共同复用原则
将经常共同复用的模块与类放在同一组件中
- 不依赖不需要用到的东西
CRP 是 ISP 的组件版
3. 组件耦合原则(Coupling)
| 原则 | 含义 |
|---|---|
| ADP:无依赖环原则 | 组件依赖结构必须无环 |
| SDP:稳定依赖原则 | 只依赖比自己更稳定的组件 |
| SAP:稳定抽象原则 | 稳定性与抽象程度应保持一致 |
组件设计的本质,是让稳定性与抽象性协同演化。系统的依赖图,是其稳定结构的投影。
ADP:无依赖环原则
组件不应该出现循环依赖
每周构建:所有人在同一个代码库中开发所有模块的代码,将一周的前几天用来开发新代码,最后一天进行模块依赖冲突的解决
为了避免循环依赖,可以通过划分独立组件进行独立开发及发布,在物理上强制隔离掉
为了消除循环依赖,有两种方法:
- 依赖反转改变组件的依赖方向
- 将相互依赖的模块提取到一个新模块
抖动:随着项目的开发,组件结构会不断扩张变化
自上而下的设计
组件结构图更像是构建性与维护性方面的地图,组件结构图一个重要的目标是如何隔离频繁的组件变更
组件的依赖关系是随着项目的逻辑设计而演进的,无法一开始就设计出完美的组件结构图
组件的依赖关系会随着架构的演进在不断变化
SDP:稳定依赖原则
一个组件只能依赖于比他更稳定的组件
稳定性
与依赖于该模块的模块数量成正比
- 越上层的组件越不稳定
不稳定性 = 依赖的组件数 / (依赖的组件数 + 依赖其的组件数)
抽象组件
- 比如只存放接口的包
抽象组件通常非常稳定
SAP:稳定抽象原则
一个组件的抽象化程度应与其稳定性程度保持一致
抽象化衡量程度
抽象程度 = 抽象类和接口的数量 / 组件中类的数量
- 痛苦区:非常稳定且非常具体
- 如数据库表结构与工具类
- 无用区:无限抽象,无人使用
4. 组件的演化与稳定性
优秀的架构并非一次性设计出来,而是在不断反馈与重构中演化的。架构师的目标不是“消除变化”,而是“使变化有序地发生”。
- 自上而下:通过边界定义控制变化。
- 自下而上:通过抽象提炼吸收变化。
- 持续重构:在变化中维护秩序。
七、设计的兼容性与演化性
稳定性不是不变,而是在变中保持秩序。
1. 协议兼容
通过版本号、编号预留等方式实现前后兼容。
2. API 兼容
保留旧接口,对外不变,内部重定向到新实现。
3. 数据兼容
旧数据的转换与兜底展示,使业务升级平滑过渡。
八、设计的未来:从模式到模型
1. 框架与 DSL
通过领域特定语言(DSL)描述意图,以“约定优于配置”的方式降低复杂性。
2. 测试与重构
单元测试是重构的基石;但“可测性”并不等于“设计良好”,二者需平衡。
3. 多次设计
比较不同方案,从多个视角评估耦合、抽象与稳定性。
九、结语:设计的哲学
软件设计的终极目标不是完美结构,而是可演化的秩序。
它既是对抗复杂性的工程技巧,也是在混乱世界中寻求秩序的思想实践。
好的设计:
- 让变化有边界;
- 让复杂性被封装;
- 让系统能够在时间维度上持续演化。
设计不是画图,而是做选择。真正的设计师,不追求完美,而追求在不完美中保持稳定。