面向对象设计原则之SOLID

前言

SOLID是面向对象设计的5个基本原则,包括了

SRP 单一职责原则
OCP 开闭原则
LSP 里氏替换原则
ISP 接口隔离原则
DIP 依赖反转原则

这些原则指导我们如何组织函数,类等代码,使程序有更高的可读性,更易被修改,更易被复用, 从而提高程序的可维护性可扩展性, 下面对这些原则依次进行介绍

单一职责原则

单一职责原则(Single Responsibility Principle)指的是

任何一个模块应该只对一类行为负责

下面举例子进行说明

有一个Person类,其有3个行为,分别是3个不同岗位的职责

Person2.png

  • 制定公司战略:属于CEO
  • 管理人力资源:属于HRD
  • 管理技术:属于CTO

虽然这些都可归为一个人的行为,但其适用场景和业务需求可能大不相同,将其耦合到一个类或模块中违反了SRP原则。对一个岗位的逻辑进行修改,很容易会影响到其他岗位的逻辑。即便另外两个岗位没有改动,保险起见也需要进行回归测试。为了避免这样的问题,需要将代码进行拆分,一个类或模块只负责一类事。

在底层编码实现上,SRP原则体现为一个函数只做一件事,在重构,提炼函数时经常使用该原则

开闭原则

开闭原则(Open Closed Principle)指的是

软件中的对象(类,模块,函数等等)对扩展是开放,对修改关封闭****

满足该要求的软件,只需少量修改或不需要修改,就能对原始需求进行修改

在设计类与模块时,可采用依赖接口或抽象类的方式进行编码,当需要扩展实现时,不用修改或只用修改少部分调用方的代码,即可完成功能扩展,其意义在于减少修改,增加系统稳定性

在软件架构层面,该项原则同样有参考意义

在一个好的软件架构下进行迭代演进,每次修改的代码会尽可能小

其中一种做法为找出业务中稳定不变的部分,作为核心层。让异变的外围部分作为业务层依赖核心层

核心层有两方面的原因使其比较稳定

  • 依赖方:核心层没有业务上的依赖,因此很少有依赖变化会导致核心层发生变化
  • 被依赖方:另一方面核心层被多个外部的业务层依赖,因此若核心层想发生变动,需要考虑业务层的兼容性问题,使得核心层的变化十分慎重。反过来说业务层的变化通常不会影响核心层

业务层也有两方面原因使其容易发生变化

  • 依赖方:只要业务层的依赖发生了变化,业务层必然需要适配变化。当前这里可以通过依赖核心层的接口而不是具体实现,以提高稳定性,参考DIP原则
  • 业务需求:业务迭代过程中需要对业务逻辑进行调整,通常改变业务层而非核心层

由此可见,核心层是最满足OCP原则,因为其几乎不会发生改变。业务层是异变的,但其影响范围有限,不会影响到核心层。在架构上将不变的部分放到内层,将易变得部分放到外层,外层依赖内层,可实现每次修改整体变动最小

里氏替换原则

里氏替换原则(Liskov Substitution principle)指的是

派生类(子类)对象可以在程序中代替其基类(超类)对象

在代码层面来说,该原则用于指导怎么实现继承

若一个子类实现了某个接口,则一定要正确实现其定义的方法。若所有子类都满足该要求,其都能在代码中替换基类

从接口使用方的角度,实现该接口的子类必然要满足接口的定义。其好处为在后续扩展上替换子类实现时,可以不用考虑新的实现类是否满足业务需求,也不需要进行特殊处理

这么说可能有点抽象,下面举一个不满足LSP原则的例子

里氏替换原则2.png

  • Set接口:定义了add和length方法,length方法返回Set中不重复元素的个数
  • Array实现类:实现了Set接口,但并没有按照接口中length方法的要求进行实现,而是返回了所有元素的个数

若使用方拿到接口代码,必然会写出如下测试

Set set = ...

set.add(1)

set.add(1)

set.add(2)

assert(set.lengh() == 2)
复制代码

根据接口Set的定义,其长度lengh()函数会返回去重后的数量。但如果实现该接口的子类不满足set的定义,也就是没有正确实现该接口,那么最后的assert必然不会成立。可以在调用方判断子类的类型并做特殊处理,但这样就不能达到子类互相替换的目的,违反LSP原则。因此最好的办法还是使得子类正确实现父类或接口

里氏替换原则是实现继承需要遵守的原则,遵循里氏替换原则,可使得在业务扩展时,调用方改动较小,不用考虑太多兼容性问题,进而增加系统稳定性

接口隔离原则

接口隔离原则(Interface Segregation Principle)指的是

用户不应被迫使用对其而言无用的方法或功能

一般情况下,一个模块依赖其不需要的功能,可能会造成困扰

有一个手机接口,有打电话call()和玩2D游戏playGame()两种功能

接口隔离原则3.png

普通人只使用其打电话功能,职业玩家只使用其打电话和打2D游戏功能

虽然普通人不需要打游戏功能,但在源代码层次以及形成了依赖关系。这意味着若以后新增打3D游戏功能,虽然和普通人没有关系,但普通人也需要感知该变动,可能造成不必要的修改降低系统稳定性

一个合理的做法是将Phone拆分成两个接口

这样一来,普通人没必要知道玩游戏有哪些功能,满足最少知识原则。其次和打游戏相关的任何变动,普通人不用感知,也就不用做相应的修改,提高系统稳定性

依赖反转原则

依赖反转原则(Dependency Inversion Principle)指的是

源代码的依赖关系中,尽量依赖抽象接口,而非具体实现

当我们修改接口层时,一定会修改实现层,反过来如果修改实现层,大概率不会修改接口层。说明接口层比实现层更加稳定。因此若依赖接口层,其变动几率小,因此比较稳定。

若对接口层进行精心设计,使其满足现阶段,及未来一段时间的业务需求,能进一步提高系统的稳定性

在软件设计中不太可能完全满足该原则,例如依赖的各种类库,包括String,Thread等类都是具体实现。但这种标准类通常十分稳定,我们不用担心其会被修改,我们应该关注更容易变动的业务层

部分设计模式遵循该原则,例如:

  • 工厂方法模式:使用抽象的工厂创建产品,方便后续替换具体工厂实现
  • 模板方法模式:用户依赖抽象父类,子类实现特定抽象方法,方便替换
  • 策略模式:用户依赖抽象的策略接口,方便后续替换策略

遵循依赖反转原则,本质是依赖更稳定模块,增加当前模块的稳定性

总结

本文详细介绍了SOLID五种设计原则的概念,以及在代码上和架构上能达成的效果,建议在设计、编写、重构代码时能够使用这些设计原则,使代码更具可读性,使系统更稳定

参考文档

维基百科-SOLID原则

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享