面向对象的编程并不能防止难以理解或不可维护的程序。因此,Robert C. Martin 制定了五项指导原则,使开发人员很容易创建出可读性强且可维护的程序。这五项原则被称为 S.O.L.I.D 原则。

  面向对象编程带来了新的软件开发设计方法。它使得开发人员能够将具有相同作用 / 功能的数据组合到一个类中,实现唯一的目的,而不管整个应用程序如何。

  但是,这种面向对象的编程并不能防止难以理解或不可维护的程序。因此,Robert C. Martin 制定了五项指导原则,使开发人员很容易创建出可读性强且可维护的程序。这五项原则被称为 S.O.L.I.D 原则(这种缩写是由 Michael Feathers 提出的):

  S:单一职责原则

  O:开闭原则

  L:里氏替换原则

  I:接口隔离原则

  D:依赖倒置原则

  下面我们将展开详细的讨论。

  注意:本文中的大多数示例可能不能满足实际情况或不能应用于实际的应用程序。这完全取决于你自己的设计和场景。最重要的是理解并知道如何应用 / 遵循这些原则。

  提示:SOLID 原则是为构建模块化、可扩展和可组合的封装组件而设计的。Bit 是实践这一原则的一个强大的工具:它能够帮助你在不同的项目中、在团队范围内轻松地隔离、共享和管理这些组件。

单一职责原则(SRP)

  一个类只应该负责一件事。如果一个类有多个职责,那么它变成了耦合的。对一个职责的修改会导致对另一个职责的修改。

  注意:这个原则不仅适用于类,也适用于软件组件和微服务。

  例如,考虑下面的设计:

  上面的 Animal 就违反了单一职责原则(SRP)。

  它为什么违反了 SRP?

  SRP 指出,类应该有一个职责,在这里,我们可以得出两个职责:动物数据库管理和动物属性管理。构造函数和 getAnimalName 管理动物属性,而 saveAnimal 管理 Animal 在数据库中的存储。

  这种设计将来会带来什么问题?

  如果应用程序的修改影响了数据库管理功能,使用 Animal 属性的类就必须修改和重新编译,以适应这种新的变化。这个系统就有点像多米诺骨牌,触碰一张牌就会影响到其他牌。

  为了使这个类符合 SRP,我们创建了另一个类,它负责将动物存储到数据库中这个单独的职责:

  在设计我们的类时,我们应该把相关的特性放在一起,这样,每当它们需要改变的时候,它们都是因为同样的原因而改变。如果它们因不同的原因而改变,我们就应该尝试将它们分开。——Steve Fenton

  恰当运用这条原则,我们的应用程序就会变成高内聚的。

开闭原则(OCP)

  软件实体(类、模块、函数)应该对扩展开放,对修改关闭。

  让我们继续以 Animal 类为例。

  我们希望遍历一个动物列表,发出它们的声音。

  函数 AnimalSound 不符合开闭原则,因为它不能对新的动物关闭。如果我们添加一种新的动物蛇:

  我们就不得不修改 AnimalSound 函数:

  如你所见,对于每一种新的动物,一段新的逻辑会被添加到 AnimalSound 函数。这是一个非常简单的例子。当应用程序变得庞大而复杂时,你会看到,每添加一种新动物,if 语句就得在 AnimalSound 函数中重复一遍。

  如何使它(AnimalSound)符合 OCP?

  Animal 现在有了一个虚方法 makeSound。我们让每一种动物扩展 Animal 类并实现 makeSound 方法。

  每一种动物都加入自己的发声方法(makeSound)实现。AnimalSound 遍历动物数组并调用每种动物的 makeSound 方法。

  现在,如果我们添加一种新动物,AnimalSound 不需要修改。我们需要做的就是把新动物加入到动物数组中。

  AnimalSound 方法符合 OCP 原则了。

  再举个例子。假如你有一家商店,你使用下面的类给自己最喜欢的客户 20% 的折扣:

  当你决定给 VIP 客户双倍的折扣(40%)时,你可能会这样修改这个类:

  这就违反了 OCP 原则。OCP 禁止这样做。如果想给不同类型的客户一个新的折扣百分比,就得添加一段新的逻辑。

  为了使它遵循 OCP 原则,我们将新建一个类来扩展 Discount。在这个新类中,我们将重新实现它的行为:

  如果你决定给超级 VIP 客户 80% 的折扣,那么代码是下面这个样子:

  就是这样,扩展而不修改。

里氏替换原则(LSP)

  子类必须可以替换它的超类。

  这个原则的目的是确保子类可以替换它的超类而没有错误。如果你发现自己的代码在检查类的类型,那么它一定违反了这个原则。

  让我们以 Animal 为例。

  上述方法违反了 LSP 原则(也违反了 OCP 原则)。它必须知道每一种 Animal 类型,并调用相应的数腿函数。

  每次创建一个新的动物类,都得修改这个函数:

  为了使这个函数符合 LSP 原则,我们将遵循 Steve Fenton 提出的 LSP 要求:

  如果超类(Animal)有一个方法接受超类类型(Anima)的参数,那么它的子类(Pigeon)应该接受超类类型(Animal 类型)或子类类型(Pigeon 类型)作为参数。

  如果超类返回一个超类类型(Animal), 那么它的子类应该返回一个超类类型(Animal 类型)或子类类型(Pigeon 类型)。

  现在,我们可以重新实现 AnimalLegCount 函数了:

  AnimalLegCount 函数并不关心传递的动物类型,它只管调用 LegCount 方法。它只知道参数必须是 Animal 类型,要么是 Animal 类,要么是它的子类。

  现在,Animal 类必须实现 / 定义一个 LegCount 方法:

  而它的子类必须实现 LegCount 方法:

  当它被传递给 AnimalLegCount 函数时,它会返回一头狮子的腿数。

  如你所见,AnimalLegCount 不需要知道动物的类型就可以返回它的腿数,它只调用了 Animal 类型的 LegCount 方法,因为根据约定,Animal 类的一个子类必须实现 LegCount 函数。

接口隔离原则(ISP)

  创建特定于客户端的细粒度接口。不应该强迫客户端依赖于它们不使用的接口。

  这个原则是为了克服实现大接口的缺点。让我们看看下面的 IShape 接口:

  这个接口可以绘制正方形、圆形、矩形。实现 IShape 接口的类 Circle、Square 和 Rectangle 必须定义方法 drawCircle()、drawSquare()、drawRectangle()。

  上面的代码很有趣。类 Rectangle 实现了它没有使用的方法 drawCircle 和 drawSquare,同样,Square 实现了 drawCircle 和 drawRectangle,Circle 实现了 drawSquare 和 drawRectangle。

  如果我们向 IShape 接口添加另一个方法,比如 drawTriangle():

  那么,这些类就必须实现新方法,否则就会抛出错误。

  我们看到,不可能实现这样一种形状类,它可以画圆,但不能画矩形、正方形或三角形。我们在实现方法时可以只抛出一个错误,表明操作无法执行。

  ISP 反对 IShape 接口的这种设计。客户端(这里是 Rectangle、Circle 和 Square)不应该被迫依赖于它们不需要或不使用的方法。另外,ISP 指出,接口应该只执行一个任务(就像 SRP 原则一样),任何额外的行为都应该抽象到另一个接口中。

  在这里,我们的 IShape 接口执行了应该由其他接口独立处理的操作。为了使 IShape 接口符合 ISP 原则,我们将对不同接口的操作进行隔离:

  ICircle 接口仅处理圆的绘制,IShape 处理任何形状的绘制,ISquare 只处理正方形的绘制,IRectangle 处理矩形的绘制。

  或者,类(Circle、Rectangle、Square、Triangle)必须继承 IShape 接口,并实现自己的绘制行为。

  然后,我们可以使用 I- 接口创建具体的形状,如半圆、直角三角形、等边三角形、钝边矩形等。

依赖倒置原则(DIP)

  依赖应该是抽象的,而不是具体的。

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

  抽象不应该依赖于细节。细节应该依赖于抽象。

  在软件开发中,我们的应用程序最终主要是由模块组成。当这种情况出现时,我们必须使用依赖注入来解决。高级组件依赖于低级组件发挥作用。

  这里,Http 是高级组件,而 HttpService 是低级组件。这种设计违反了 DIP A:高级模块不应该依赖于低级模块。它应该依赖于它的抽象。

  该 Http 类被迫依赖于 XMLHttpService 类。如果我们要修改 Http 连接服务,也许我们想通过 Nodejs 连接到互联网,甚至模拟 http 服务。我们将不得不费力地遍历所有 Http 实例来编辑代码,这违反了 OCP 原则。

  Http 类不应该关心使用的 Http 服务的类型。我们做了一个 Connection 接口:

  Connection 接口有一个 request 方法。有了这个接口,我们就可以向 Http 类传递一个 Connection 类型的参数:

  因此,无论传递给 Http 类的 Http 连接服务是什么类型,它都可以轻松地连接到网络,而无需知道网络连接的类型。

  现在,我们重新实现 XMLHttpService 类来实现 Connection 接口:

  我们可以创建许多 Http 连接类型,并将其传递给 Http 类,而不必担心错误。

  现在,我们可以看到,高级模块和低级模块都依赖于抽象。Http 类(高级模块)依赖于 Connection 接口(抽象),而 Http 服务类型(低级模块)也依赖于 Connection 接口(抽象)。

  此外,DIP 原则会强制我们遵循里氏替换原则:Connection 类型 Node-XML-MockHttpService 可以替换它们的父类型连接。

小结

  本文介绍了每个软件开发人员都必须遵守的五个原则。首先,要遵守所有这些原则可能会令人生畏,但是随着不断的实践和坚持,它们会成为我们的一部分,并将对应用程序的维护产生巨大的影响。

  关于这些原则,如果你觉得有什么需要添加、纠正或删除,请在下面的评论区留言,我非常乐意与你讨论!

每个Web开发者都应该知道的SOLID原则的更多相关文章

  1. 每个开发者都应该知道的SOLID原则

    每个开发者都应该知道的SOLID原则 单一职责原则(SRP) 它为什么违反了 SRP? 这种设计将来会带来什么问题? 开闭原则(OCP) 如何使它(AnimalSound)符合 OCP? 里氏替换原则 ...

  2. HTTPS是如何保证连接安全:每位Web开发者都应知道的

    “HTTPS协议的工作原理是什么?”这是我在数天前工作项目中需要解决的问题. 作为一名Web开发者,我当然知道 HTTPS 协议是保障用户敏感数据的好办法,但并不知道这种协议的内在工作机制. 它怎么保 ...

  3. 每一个JavaScript开发者都应该知道的10道面试题

    JavaScript十分特别.而且差点儿在每一个大型应用中起着至关关键的数据.那么,究竟是什么使JavaScript显得与众不同,意义非凡? 这里有一些问题将帮助你了解其真正的奥妙所在:   1.你能 ...

  4. 每个 Java 开发者都应该知道的 5 个注解

    自 JDK5 推出以来,注解已成为Java生态系统不可缺少的一部分.虽然开发者为Java框架(例如Spring的@Autowired)开发了无数的自定义注解,但编译器认可的一些注解非常重要. 在本文中 ...

  5. 【译】前端开发者都应知道的 jQuery 小技巧

    回到顶部按钮 通过使用 jQuery 中的 animate 和 scrollTop 方法,你无需插件便可创建一个简单地回到顶部动画: // Back to top $('a.top').click(f ...

  6. 每个极客都应该知道的Linux技巧

    每个极客都应该知道的Linux技巧 2014/03/07 | 分类: IT技术 | 0 条评论 | 标签: LINUX 分享到:18 本文由 伯乐在线 - 欣仔 翻译自 TuxRadar Linux. ...

  7. 开发者所需要知道的iOS7 SDK新特性

    iOS 7 春风又绿加州岸,物是人非又一年.WWDC 2013 keynote落下帷幕,新的iOS开发旅程也由此开启.在iOS7界面重大变革的背后,开发者们需要知道的又有哪些呢.同去年一样,我会先简单 ...

  8. 隔壁小孩都要知道的Drupal配置

    i春秋作家:Arizona 原文来自:隔壁小孩都要知道的Drupal配置 隔壁小孩都要知道的Drupal配置 Drupal是一个开源的PHP内容管理系统,具有相当复杂的架构.它还具有强大的安全模型.感 ...

  9. PDB文件:每个开发人员都必须知道的 PDB Files

    PDB文件:每个开发人员都必须知道的   PDB Files: What Every Developer Must Knowhttp://www.wintellect.com/CS/blogs/jro ...

随机推荐

  1. 请转发!简单2分钟制作无接触式小区进出微信登记表!全免费!数据安全!所有数据均存在创建人登录的QQ腾讯文档里!

    全免费!数据安全!所有数据均存在创建人登录的QQ腾讯文档里! 阻击疫情到了最吃劲的关键期,大家能不出门就不出门,但免不了出去买个菜.取个快递啥的,每次出入的时候,社区同志都在认真拿着笔记录每个进出信息 ...

  2. ASP.NET Core 2.2 WebApi 系列【九】使用SignalR (作者:tenghao510 ) 学习及内容补充

    原文地址:  ASP.NET Core 2.2 WebApi 系列[九]使用SignalR 今天,看到了大牛的这篇博文,  发了一下评论, 我很惊喜, 没想到他很快就回复了我,  而且通过QQ帮助了S ...

  3. create-react-app 打包后静态文件过大 webpack优化

    在最近的项目里,页面和静态文件并不是很多的情况下,打包后发现产出的静态资源却很大. 1.关掉sourcemap 在config/webpack.config.js文件里,大概30几行的位置添加这样一句 ...

  4. [Effective Java 读书笔记] 第6章 枚举和注解

    第三十条 用enum代替int 总得来说,使用enum有几点好处 1.编译时的类型安全, 2.可以保证就是自己定义的值,不会有月结风险, 3.每个枚举类型有自己的命名空间 4.枚举可以添加任意的方法和 ...

  5. qt creator源码全方面分析(2-3-2)

    目录 Showing Task List Files in Issues Pane 管理任务列表条目 任务列表文件格式 Showing Task List Files in Issues Pane 您 ...

  6. JS生成全局唯一标识符(GUID,UUID)的方法

    全局唯一标识符(GUID,Globally Unique Identifier)也称作 UUID(Universally Unique IDentifier) . GUID是一种由算法生成的二进制长度 ...

  7. table-cell设置宽高、居中

    table-cell默认宽高由内容决定 <style type="text/css" rel="stylesheet"> .content { co ...

  8. uniapp简易直播

    实验准备 在服务器部署nginx-rtmp作为我们直播推流和拉流的服务器(如果服务商选择七牛,也是直接给地址推流).为了加快部署,我在这一步使用Docker. docker pull tiangolo ...

  9. redis的基础知识

    select切换数据库 remoteSelf:0>select 0 "OK" dbsize查看当前数据库的key数量 remoteSelf:0>dbsize " ...

  10. Ubuntu系统下使用php7+mysql+apache2搭建自己的博客

    很多人都有写博客的习惯,奈何国内的博客网站正在一家家地关闭与重整,部分博客网站也充斥着太多的广告,使用体验非常不好.对于爱写博客的朋友来说,其实还有一个更好的选择,那就是自己搭建一个博客. 搭建一个自 ...