SOLID简介:
历史:由Robert C·Martin汇总并推广
目标:
- 使软件更容易被改动
- 是软件更容易被理解
- 构建可以在多个软件系统中复用的组件
组成:
名称 | 简写 | 含义 |
单一职责原则 | SRP Single Responsibility Principle | 初始定义(2004):任何一个软件模块都应该只有一个职责;或者说都应该只有一个变化的原因 修订定义(2018):任何一个模块都应该只对某一类行为者负责 |
开放关闭原则 | OCP Open-Close Principle | 软件实体(类、模块、函数)是可以扩展的,但是不可修改 |
里式替换原则 | LSP Liskov Substitution Principle | 子类型必须能替换他们的基类型,且不改变原有逻辑 |
接口隔离原则 | ISP Interface Segregation Principle | 接口应该是内聚的; 不应该强迫客户依赖于他们不用的方法 |
依赖倒置原则 | DIP Dependency Inversion Principle | 高层模块不应该依赖于底层模块,二者都应该依赖于抽象; 抽象不应该依赖于细节,细节应该依赖于抽象 |
单一职责原则
定义:
初始定义(2004):任何一个软件模块都应该只有一个职责;或者说都应该只有一个变化的原因。
修订定义(2018):任何一个模块都应该只对某一类行为者负责。
设计初衷:
设计出更内聚的概念(模块、类、函数),从而降低多个调用者之间的耦合。耦合有两类表现:
落地指引:
- 类中含有过多的属性或方法,或者代码行比较多,可能是一个类需要拆分的提示;
- 依赖它的类比较多,且依赖的接口大多不重合;或者它依赖的类比较多,也是一个类需要拆分的提示;
- 比较难给类起名字,只能用笼统的agent/manager来命名,也从侧面说明该类职责过多,需要拆分;
- 从被复用的粒度考虑,如果该类中有部分概念总是被单独复用,那么这些概念可能需要单独拆分出来。
防止过度设计:
SRP是从行为者(使用者)角度来指导设计,我们还需要从提供者(维护者)角度同步思考:易于修改。
开放封闭闭原则
定义:
软件实体(类、结构、方法等)应该“对扩展开放,对修改关闭”。进一步描述就是添加一个新的功能时应该在现有代码基础上扩展代码(新增模块、类、方法等),而非修改现有代码(修改模块、类、方法等)。
修改:在现有函数、类中改动,且原有代码的使用者被迫感知这一变化
- 被迫编译、构建
- 被迫对齐修改(接口重新适配、对齐)
- 被迫重新测试(修改现有用例)
扩展:新增函数、类等,原有代码的使用者不感知
关键点:对老用户的影响
设计初衷:
设计出可扩展性更好的架构,从而降低或消除高层组件对底层组件的依赖。
- 可扩展性是衡量架构设计质量的重要维度之一,大部分的设计模式、方法都是为了解决可扩展性问题而总结提炼出来的。
- 为了低成本的应对需求变化,需要对扩展开放;为了保证现有代码的稳定性,需要对修改关闭。
- 修改不可避免,需要做的是尽量让最核心、最复杂的那部分逻辑满足OCP,将易变的部分通过抽象隔离在低阶组件上。
落地指引:
防止过度设计:
- 具备扩展意识、抽象意识、封装意识(高层原则)
- 需要熟悉业务,了解业务变化的方向
- 需要熟悉常见的设计模式或方法
- “分离关注点”是关键:基于接口或抽象实现“封闭”,基于实现接口或继承实现“开放”
- 底层方法论(设计模式)
- 设计方法:常见的方法有多态、依赖注入、基于接口或抽象编程
- 设计模式:常见的有策略、装饰、职责链等
- OCP是有代价的
抽象后会影响代码的可读性,特别是对于复杂逻辑来说,为了达到OCP会引入很多的中间件来降低依赖。
里式替换原则
定义:
子类对象能够替换程序中任何地方出现的父类对象,并且保证原来程序的逻辑行为不变(正确性不被破坏)
应用范围:面向对象编程中的类和它的子类、接口和它的实现
原则中的限制:
替换后没有改变原来程序的逻辑行为及其约束,可能包括:
- 功能约定
- 输入约定
- 输出约定(例如是否抛出异常)
- 以及在父类的注释中提出的其他约束
精髓:degisn by contract,按照契约来设计
落地指引:
举反例如下:
- 子类是否违背父类声明要实现的功能
父类中提供的订单排序函数,是按照金额大小从小到大给订单排序的,而子类重写这个函数的时候,是按照订单排序日期来给订单排序的
- 子类违背父类对于输入、输出、异常的约定
- 子类违背父类注释中所罗列的任何特殊说明
一旦违反了LSP,系统就不得不为此添加大量复杂的应对机制。
接口隔离原则
定义:
用户(调用者/客户端)不应该被迫依赖它不需要的接口。
设计初衷:
接口隔离原则是为了降低多个调用者之间的耦合(被迫依赖倒置的耦合):
- 被迫编译、构建
- 被迫对齐修改(接口重新适配、对齐)
- 被迫重新测试(修改现有用例)
和单一职责的区别?
- 更具体:可以说是单一职责在接口上的应用
- 如果说单一职责原则是为了更好的内聚,那么接口隔离原则是为了更好的“解耦”
- 单一职责面向的是模块、类、函数,且是从自身设计出发不要设计出大而全的模块、类、函数,强调内聚
- 而接口隔离原则面向的是接口,更多的是从接口的用户(调用者)出发,避免用户依赖了它不需要的逻辑,强调解耦
- 可以说它提供了一种判定接口设计是否满足单一职责的方法
落地指引:
- 通过物理拆分实现接口隔离
- 通过适配模式提供需要的接口给用户
使用适配模式前:
使用适配模式后:
- 通过多继承方法或Facade模式提供接口给用户
Facade(外观)模式为子系统中的各类(或结构与方法)提供一个简明一致的界面,隐藏子系统的复杂性,使子系统更加容易使用
防止过度设计:
- 为了达到接口隔离而把大接口拆分成多个小接口可能会导致接口逻辑离散化;
- 接口的粒度大小取决于调用者的需求和内聚性概念。如果用户只有一个,基本上所需即所得,一般不会存在接口隔离的需要;
- 为了满足接口隔离原则进行物理拆分时,不能仅从各个用户的需求出发,而是要同时考虑接口逻辑的内聚性,可以参考单一职责原则中的“防止过度设计”小节。
依赖倒置原则
定义:
高层模块(类)不应该依赖低层模块(类),二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
关键概念:
- 什么是高层、什么是低层:在一个调用链层次中,我们称调用者(用户)为高层概念,被调用者(服务提出者)为低层概念。
- 什么是抽象、什么是细节:抽象是现实概念背后的本质,细节是当前用到的现实概念;抽象可以用接口/抽象类来代替,细节可以用具体类或实现来代替。
- 依赖:调用即依赖。代码依赖方向和程序控制流方向相同,称为正常依赖。
- 倒置:如果相反,就是依赖倒置。
设计初衷:
保持核心逻辑的稳定:
- 高层为业务逻辑层(策略层),低层为实现层,一般来说高层的变动比低层频繁,因此我们不期望频繁的变化影响低层实现,虽然这些频繁的变动是可以带来价值的。
- 低层的变动一般是技术选型、外部依赖等导致的变动,一般是不带来价值的,因此我们也期望低层的变动尽量不影响高层业务逻辑。
- 核心逻辑一般是稳定的,所以它应该被找出来,作为高层和低层共同的依赖。(分离关注点)
落地指引:
- 依赖倒置原则的核心是面向接口编程
应该在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类
任何变量都不应该指向一个具体类
任何类都不应该继承自具体类
任何方法都不一样改写父类中已经实现的方法
- 可以采用一些工具生成项目的依赖关系,作为架构重构的指引
- 依赖倒置可以通过依赖注入来实现
防止过度设计:
- 上述提到的落地指引比较绝对,现实中是无法做到的,因为最终都是要靠具体类来完成具体业务执行的。
- 折中的判断原则是依赖的对象是否稳定(正因为抽象是稳定的,我们才会去依赖抽象),比如标准库中的类相对是稳定的,String/Vector等,我们就可以直接依赖。
参考: