什么是SOLID原则?

以Robert C. Martin的《Clean Architecture》为参考,我们可以这样说:

好的软件系统始于清晰的代码。一方面,如果砖头做得不好,建筑的架构就不太重要了。另一方面,你可以用制作精良的砖头制造出相当大的混乱。这就是SOLID原则发挥作用的地方。

SOLID原则告诉我们如何安排我们的函数和数据,原则的目标是创建容忍变化、易于理解的、可用于许多软件系统的组件的心理级软件结构。

好的,现在我们已经对SOLID有了一个概述,现在我将告诉你SOLID的每个原则,这些原则如下:

  • 单一职责原则:

“一个模块应该只有一个修改的理由”

  • 开闭原则:

“软件工件应该开放扩展,但关闭修改”

  • 里氏替换原则:

“所要求的是一种类似于以下替换属性的东西:如果对于类型S的每个对象o1,都有一个类型T的对象o2,使得对于所有在T中定义的程序P,当o1替换o2时,P的行为不变,则S是T的子类型”

  • 接口隔离原则:

“客户端不应被迫依赖于他们不使用的方法”

  • 依赖反转原则:

“高级模块不应依赖于低级模块。两者都应该依赖于抽象。抽象不应依赖于细节。细节不应依赖于抽象”

我们将在下一部分基于每个原则。

SOLID和Go

正如我们在前面的部分中所看到的,SOLID是为面向对象编程而设计的,但这并不是像Golang这样的语言的限制,我将向你展示如何在你的Golang项目中安排和实现它。

让我们从每个原则开始,定义如何在Golang中实现它们。

单一职责原则 — SRP

— 定义:“一个模块应该只有一个修改的理由”

我们已经知道这个原则的含义,现在是时候学习如何在Golang中实现它了。

对于Golang,我们可以将此原则定义为_“一个函数或类型应该只有一个任务和一个职责”,_告诉我们是时候通过一个例子来看看它是如何实现的:

有了下面的代码:

我们可以看到这不是一个糟糕的代码,但是,代码中发生了什么呢?如果我们停下来读一下14和23行的area方法,我们会发现一些事情,代码正在计算面积并打印结果,它破坏了“单一职责原则”。

现在,应用“单一职责原则”:

  • 步骤1:修改方法只执行一个动作,例如这里只计算面积
  • 步骤2:我们可以定义一个新类型来处理面积输出,例如,在我们的例子中,我们想将其打印在控制台上。然后我们将字符串转换委托给一个名为outPrinter的新类型和方法,它有一个方法返回带有形状面积的字符串。

在第11行,我们将形状接口作为参数传递,它帮助我们使用实现接口方法的任何类型,在这种情况下,是定义面积方法的正方形和圆形类型。

  • 步骤3:最后,在main函数中,我们可以执行以下操作:

正如我们在示例中看到的,代码将代码定义为具有单独职责的代码。一方面,正方形和圆形类型定义了带有计算面积和返回值的方法area;另一方面,类型outPrinter将定义生成所需字符串输出所需的所有内容。

开闭原则 — OCP

— 定义:“软件工件应该开放扩展,但关闭修改”

我们从下面的代码开始:

上面的代码没有实现“开闭原则”,原因是:

  • 计算器类型具有“sumAreas”方法,该方法定义了“shapes”的接口{}类型的参数。我们可以找到一个switch case映射每个可能的类型,这破坏了原则,因为在定义新的形状时,“triangle”例如,我们将需要修改“sumAreas”方法以处理新类型。

如何解决?

我们可以遵循以下方法:

  • 定义具有area方法的“shape”接口
  • 我们的示例类型之一定义了方法“area”

- 另一个需要更改的地方是计算器的“sumAreas”方法,这次参数被定义为形状,switch-case被删除,代码执行每个形状的“area”方法。

下面是使用新实现的方法:

里氏替换原则 — LSP

— 定义:“这里需要的是像以下这样的替换属性:如果对于类型S的每个对象o1,都存在类型T的对象o2,使得对于所有以T为基础定义的程序P,当o1替换o2时,P的行为不会改变,则S是T的子类型”

Golang没有继承,但有组合。我们可以组合多个结构体,并且它将适用于“里氏替换原则”的示例。让我们看看以下代码示例:

在上面的代码中,我们可以分析以下内容:

  • 代码定义了“vehicle”类型,该类型定义了接口方法“getName”。
  • “Car”和“motorcycle”使用组合访问“vehicle”,这意味着除了访问属性外,汽车和摩托车还可以访问“vehicle”方法。
  • 打印机方法将与车辆、汽车和摩托车一起使用。

总之,我们可以说以下内容:“里氏替换原则”适用,因为“car”类型和“motorcycle”类型可以被“vehicle”类型替换。

在以下代码中,我们可以看到主函数和控制台输出。

接口隔离原则 — ISP

— 定义:“客户端不应被迫依赖于他们不使用的方法”

简单地说,我们可以说“保持接口简单,最好只有一个方法”。

在以下示例中,我将向你展示如何在Golang中应用此原则:

让我们开始,在下面的图像中,我们可以看到定义了一个接口,其中包含两种方法,一种用于区域,另一种用于体积,直到现在都很好。

现在,正如我们在下面的图像中所观察到的那样,代码定义了两个形状,一个正方形和一个立方体,每个形状都实现了area()和volume()方法。

这里有两个函数,一个用于求面积,另一个用于求体积。

代码看起来很好,但是如果我们稍微分析一下并记住原则“客户端不应被迫依赖于他们不使用的方法”,我们可以在形状中看到以下内容:

  • 正方形不需要体积方法,因为它是一个平面形状
  • 只有立方体需要体积方法,因为它是一个物体形状

基于这些要点,我们可以通过实现以下更改来修复代码:

  • 添加一个名为“shape”的新接口来定义“area()”方法。
  • 为“对象”形状添加新接口以定义“volume()”方法,并与“shape”接口组合,以允许使用“area()”方法。

在我们的示例中,正方形实现了“area()”,立方体实现了“area()”和“volume()”,这符合“接口隔离原则”,因为我们正在拆分接口,以不强制它们使用或定义形状不会使用的方法,在示例中是正方形。

最后一个更改,求面积和体积的函数需要更改为只接受正确的形状。

依赖倒置原则 — DIP

— 定义:“高级模块不应依赖于低级模块。两者都应该依赖于抽象。抽象不应该依赖于细节。细节不应该依赖于抽象”

这里有一个示例来更详细地解释该原则的含义。

在下面的图像中,我们可以看到一个非常基本的示例,该示例实现了“数据库”连接和用于查询数据的存储库。这段代码没有遵循依赖倒置原则,因为它在仓库中定义了用于访问查询方法的结构类型。

现在,通过实现“依赖倒置”原则,代码进行了以下修改:

  • 添加了一个新接口来定义数据库方法
  • 修改了仓库,不再定义类型为“MySQL”的属性,而是定义了接口类型的抽象。

在下面的图像中,我们可以看到基于原则定义所做的更改:

注:我们可以使用工厂来初始化仓库属性以改进代码。

结论

作为软件开发人员,我们应该始终寻找编写最佳代码的方法,将SOLID原则应用于我们的项目中将帮助我们编写更严谨、可扩展和容错的代码,因为我们正在正确地定义应用程序的基础。

推荐阅读:

  • 《Clean Architecture》 by Robert C. Martin
  • 《Design Patterns》 by Erich Gamma