本博文碍于作者的学识与见解,难免会有疏漏错误之处,请谅解。

转载请注明出处: https://www.morcat.cn 谢谢~

前言

今天呢,我想来谈谈我对大名鼎鼎的"SOLID"五大设计原则的理解。所有的设计模式都是围绕着设计原则展开的,如果说繁杂的设计模式是武林中的降龙十八掌,那么设计原则就相当于顶级内功心法————九阴真经。好吧我承认这个比喻有点奇葩,我就想表达下对设计原则的理解程度会影响到你代码和架构的质量。

当然啦,作为一个刚出新手村的小白程序猿自知理解肯定不是很深刻,希望有不足的地方都帮我指出来(如果有人看的话...)

SRP:单一职责原则

任何一个软件模块都应该有且仅有一个被修改的原因

软件模块 大部分情况下指的是一个源代码文件,在面向对象编程中一般也就是"类"。我个人理解往大里说一个成一个系统一个服务也是可以的。

我们一直强调软件设计需要做到高内聚,低耦合。其实这就是要我们遵守单一职责原则,一个类只有一个发生变化的原因那就自然导致了类中所有的信息是相关性非常高的。

遵循这个原则可以让我们代码的可维护性变高,试想一下如果某个类要同时对许多不同的行为负责。那么变更的可能性会非常大,甚至会存在同时变更同一个类的情况,风险会很高。

如何做好SRP原则?

我觉得做好SRP的一个很核心的点就是 聚合和拆解 ,我们需要将相同的职责聚合到一起,不同的职责拆解到不同的接口和实现中去。

但是每个人所理解的职责是不一样的,在不同的业务场景中,同一个类的聚合和拆解的粒度也可能是不同的。我们在应用时需要对职责进行清晰的定义,根据自己的需求合理的拆分职责,这也是这个原则运用的一个难点。

举个栗子:

以电脑为例,一个电脑可能会有许多属性如:品牌,价格等,也会有许多功能如:比如开机,关机,写文本,听音乐等等。如果把所有功能都放在一起,我们的类将会是这样:

image.png

其实在工作中,这样子的类是非常常见的,我们把所有与电脑相关的东西全部当做一个职责,如果需求从此不再变化似乎也是可以如此的。但是我们需要认真考虑的是,在这个电脑实现功能中有哪些部分是易变的呢?电脑是一个很复杂的组成,我们这里就简单分成两部分,一部分是底层硬件相关的如电源,硬盘等。还有一部分是上层应用相关的如文本文档,音乐播放器等。很明显当底层硬件电源参数改变时会影响到开关机,但不会影响到写文本,放音乐等功能(其实底层硬件如果坏了还是会影响上层软件的,这里就假设下别杠)。上层应用接口发送改变,会影响到写文本,放音乐等功能,但不会影响到开关机。如此一来,我们会发现其实这个类在的职责是不单一的,他会受到底层硬件和上层应用两方面的影响。这个时候我们就需要做拆分:

image.png

通过将底层硬件与上层应用拆成两个接口,每个接口只负责他自己职责内的改变。当然,如果需求变化的维度非常大,比如应用层实际可能还需要拆成游戏应用层,音乐应用层。不是说功能拆的越细越好,拆的越细代码的复杂度也就越高。因此很重要也是我个人认为遵循该职责最难的一点是我们需要根据我们的需求合理拆解软件模块的职责,当然这说起来似乎很容易,但是需求一直在变,所以一个好的架构师往往能提前感知到哪些模块是易变的,哪些是不变的,从而提前做出应对,设计出合理的代码架构。

OCP:开闭原则

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

开闭原则是我认为的五大设计原则中最为重要的设计原则,是我们软件架构中想要做到的终极目标。其他的设计原则其实也都是为了实现开闭原则而做的拓展。

讲一下为什么那么重要吧,我们做架构设计的根本原因其实就是为了让我们的软件系统能够很好的应对各种需求变化,让系统更加易于扩展,同时又限制其每次被修改所影响的范围,从而使得软件系统的寿命可以更长,更稳定(这也是为什么我不推荐去外包公司的原因,外包公司做的产品大部分都是一次性的,不会去考虑到这些)。而一个架构最完美的状态是什么,就是要做到任何需求的改变都只需要加新的代码,老的代码根本不需要动。当然,这几乎是不可能的,但是他并不影响我们将该原则作为我们系统设计的追求目标。

如何做好OCP原则?

在《架构整洁之道》一书中,Bob大叔提出了一种做法:

将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响

说起来还挺玄乎的,其实这是对两个设计原则的运用。将系统划分为一系列组件 运用的思想其实就是上面所述的SRP原则。而 将这些组件间的依赖关系按层次结构进行组织 运用的思想是后面会介绍的DIP依赖导致原则。

LSP:里氏替换原则

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

我个人理解里氏替换原则是用来指导我们合理使用继承的一种思想。面向对象编程有一个很重要的特性就是 继承 。继承是一把双刃剑,他给我们的编程带来了很大的帮助有效减少了大量的重复代码,同样的也会使类与类的耦合度增强。

而里氏替换原则原则就是要规范继承的使用方式,规避继承带来的弊端使得软件复杂度上升。里氏替换原则通俗点讲就是子类可以扩展父类的功能,但不能改变父类原有的功能。更直白点就是子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

给一个最为经典的反例

image.png

图中Square类继承了Rectangle类。但是Square的高和宽必须是一起修改的和Rectangle父类不一样,上层Client在使用时由于不感知Square子类,因此会带来混淆与风险。因此这就是一个很典型的违反LSP原则的案例。想要解决的话,要么在Client上直接加If else判断类型使用,但是这样的话就失去了继承本身带来的便利。问题的本质其实是,Square根本不应该的Rectangle的子类,这样的定义本身就是错误的,二者应该都为Shape的子类。

当我们使用继承时,最好仔细看看自己的子类与父类的关系是否符合里氏替换原则,如果不符合就需要斟酌一下这种使用方式是否合理了。

ISP:接口隔离原则

客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上

ISP应该还比较好理解,该原则指导我们定义接口时尽可能拆的细一点,尽量不要实现自己不需要使用的接口。如下图所示,Client1只需使用method1方法,Client2只需使用method2方法,Client3只需要使用method3方法,那么大可不必都依赖OPS实现类,这样反而使得使用的风险变高,同时对method1方法的改动会影响到根本不需要使用它的Client2,Client3。因此可以将不同的操作隔离成接口来解决此类问题。

image.png

DIP: 依赖倒置原则

抽象不应当依赖于细节;细节应当依赖于抽象。

之前说过DIP原则是OCP开闭原则的一个很重要的实现方式。这个原则实际是鼓励我们要针对接口编程,不要针对实现编程。

如何做好DIP原则?

这里再次引用了《架构整洁之道》一书中的四项编码规范:

  1. 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
  2. 不要在具体实现类上创建衍生类。
  3. 不要覆盖包含具体实现的函数。
  4. 应避免在代码中写入与具体实现相关的名字,或者是其他容易变动的事物的名称。

举个栗子:

我们平时做传统分层架构时其实已经使用了DIP原则了。如下图所示,Service层仅依赖了下层Repository抽象,而下层的具体实现也依赖Repository抽象可自行决定实现方式与拓展,这也就是
倒置的含义。

image.png

结语

写完这篇文章后我其实对SOLID原则有了一个更深的认识,但也发现自己能力确实还有待提高,有些含义我也知道介绍的不是很清晰,可能自己理解也不够透彻吧。鬼知道我这篇文章竟然断断续续写了快3周,希望能给刚接触设计模式的同学一点参考吧。

Q.E.D.


吃的苦中苦,卷成王中王🏆