> For the complete documentation index, see [llms.txt](https://shniu.gitbook.io/cs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://shniu.gitbook.io/cs/system-design/design/02-she-ji-yuan-ze.md).

# 设计原则

> 时刻保持动态的眼光来看待这些设计原则

## SRP 单一职责原则

SRP，Single Responsibility Principle. 这个原则的核心是：一个类或者模块只负责完成一个职责（或功能）。

**解读**：

* SRP 并不好把控，没有一个放之四海而皆准的标准；有时候也理解为只有一个原因引起类或模块的变化，都非常抽象和模糊，并不具体，所以 SRP 真的去落地到实际项目就会有难度；所以，职责划分是 SRP 最难的，如何找到职责边界，如何落地，在不同的情境下对职责的理解和考量又不尽相同
* 不同需求阶段，不同应用场景，不同业务角度去理解和应用 SRP 都是不一样的，这个需要通过大量的实战来总结经验，找到 SRP 的“感觉”；但是可以采取持续重构的方式来应对 SRP，尽量让模块和类的设计满足它，可以作为设计时的指导原则和意识。
* 在实际应用中，不要设计大而全的类，要设计粒度小、功能单一的类，如果某个类或模块承担了过多的职责，那就可以考虑将它拆分成多个类或模块；可以说 SRP 是高内聚的一种体现或指导原则，把相同的功能或职责内聚到一个类或模块中，不同类和模块之间以一种代价最小的方式组装在一起，类和类、模块和模块之间必定存在联系，如果没有联系的两件东西放在一起毫无价值，而这种联系的强弱和稳定度是需要重点考虑的，SRP 是从职责和功能的角度来界定内聚的标准和耦合的尺度，告诉我们做设计时要从自己理解的角度去做需求拆分、功能拆分、子系统拆分、模块拆分、类的拆分、方法的拆分，而不是用一个大而全的东西包含所有的，拆分能带来职责明晰，多方协作，这不正是真实世界的运作机制吗

其实，我很想能思考明白 SRP 的本质是什么，但是它确实太抽象太模糊，我认为的如何应用 SRP 的一般方法：

1. 首先要有 SRP 的意识，总结为意识先行；
2. 其次善于利用持续重构的方式，不要做过度设计，做符合当前需求和情景的设计，尽量保持类或模块的小粒度和功能单一，总结为重构开路；
3. 最后总结经验，培养感觉，在实战中提升，总结为积累经验；

SRP 意识先行，持续重构开路，实战中积累经验。

简单理解 SRP 就是要从职责的层面做到分工明确。

## OCP 开闭原则

OCP 说的是对软件实体（模块、类、方法等）应该对扩展开放，对修改关闭。

OCP 并不是说不需要修改代码，而是从软件设计和功能语义层面去理解，要认识到，添加一个新功能，不可能任何模块、类、方法的代码都不“修改”，这个是做不到的。类需要创建、组装、并且做一些初始化操作，才能构建成可运行的的程序，这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层，尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。比如说骨架需要是稳定的，这里就不要做过多修改，而枝叶是多种多样的，这里就要做好灵活的扩展机制了。再通俗点的理解：我们有一个稳定的结构，在这个基础之上可以支持尽可能多的扩展能力，扩展能力就是对扩展开放，稳定结构就是对修改关闭。举个例子，CPU 大家都了解，CPU 的设计就符合 OCP，CPU 分为计算单元、控制单元、数据单元和一系列指令，这个结构是稳定的，CPU 进化了那么多年依然是这么工作的，但是 CPU 的能力可以无限扩展的，CPU 可以处理文本数据、图像、音视频等等，但是都不需要修改 CPU 本身。

OCP 是一个指导原则，指导我们设计出有稳定结构且可以无限扩展的架构，更好的将可变部分和不变部分隔离开。做到 OCP 不是一件容易的事情：

1. 首先要有抽象、封装、扩展的意识，有这些意识是做好事情的前提；就是在做的时候，花点时间去多思考，不同角度反复推敲，比如未来哪些需求可能会变，是不是要留扩展点，代码结构怎么设计，单测怎么设计，会不会严重违反某些好的指导原则等
2. 多态、依赖注入、基于接口而非实现编程、抽象等都是实现 OCP 的具体方式
3. 一些常见的设计模式也可以做到 OCP

为什么我们要“对扩展开放、对修改关闭”？

做任何事情都需要进行积累，从0到1，从1到2，从2到3...这样一步步的完成我们的目标，这个过程中就需要有一个稳定的地基来承载每一步带来的变化，当然地基本身的稳定性也是一点点重构出来的，随着理解的加深，地基就越来越稳定，新的需求在已有的功能上进行迭代，这是我们追求的最好的样子，不浪费以前的工作，继续开展新的工作，日积月累完成最终的目标。所以 OCP 就是这样一种指导原则，提醒我们在做事情时要识别稳定点和变化点，将稳定点满满的沉淀为稳定的地基，将变化点演变为灵活的扩展点，只有这样才是最高效的。这样我们就可以利用抽象意识，多态、依赖注入、面向接口编程等很多方式去落地代码，达到不浪费以前工作，继续基础新功能的目的。

## LSP 里式替换原则

LSP 是这样描述的：子类对象（object of subtype/derived class）能够替换程序（program）中父类对象（object of base/parent class）出现的任何地方，并且保证原来程序的逻辑行为（behavior）不变及正确性不被破坏。

LSP 有两个关键点：

1. 子类可以替换父类
2. 要保证原来程序的逻辑行为（behavior）不变及正确性不被破坏

时刻抓住这两点，就能很好判断一段程序或者一个设计是否符合 LSP。如果子类替换父类后产生了协议之外的副作用，就说明违背了 LSP，可能会造成很多未知的错误，或者引发其他系统的错误。

LSP 告诉我们一定要严格按照协议设计来实现，LSP 可以用于接口设计、类设计、方法设计等。

此外，多态和里式替换有点类似，但它们关注的角度是不一样的。多态是面向对象编程的一大特性，也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则，是用来指导继承关系中子类该如何设计的，子类的设计要保证在替换父类的时候，不改变原有程序的逻辑以及不破坏原有程序的正确性。

## ISP 接口隔离原则

ISP 是这样描述的：客户端不应该强迫依赖它不需要的接口。其中的“客户端”，可以理解为接口的调用者或者使用者。

ISP 的核心是来指导接口的设计，告诉我们要结合具体使用场景，侧重于如何设计接口才是更加合理的，ISP 给了一个判断标准，如果使用者只依赖接口的部分功能，就需要把接口的功能拆分成多个接口，进行细化；如果使用者只依赖部分接口，那就需要把其他接口功能做隔离，放到其他接口的上下文中
