分享

一文带你彻底弄懂SOLID原则

 好汉勃士 2023-04-18 发布于广东

前言

在当今不断发展的世界中,客户需求以前所未有的速度不断变化。软件团队必须适应新需求并迅速交付更改。为此,减少软件开发和测试时间很有必要。同时,每隔一年都会推出新技术。通过替换现有技术来试验更优化、更高效的技术是很常见的。因此,编写的代码高内聚且松耦合十分重要。

好在著名大师Robert Martin(Bob 大师)提出的SOLID软件设计原则,给了我们正确的指导,我们将逐步介绍这五项原则,并为每一项原则进行说明。

S—单一职责原则

这是最容易理解的原则之一。它指出“一个类必须只有一个改变的理由”。很多时候,您可能会发现一个类执行的功能多于它应该执行的功能。

假设您正在为银行软件编写代码。功能是显示给定用户的声明。该代码从数据库中获取数据并以用户选择的格式显示数据,看看下面的代码有什么问题。

文章图片1

银行报表管理器

从上面的代码片段中可以看出,BankStatementMgr类同时执行多项操作。它从数据库中获取数据,解析结果,然后以用户指定的格式显示它。可以发现它存在以下的几个问题:

  • 没有职责分离。如果引入新格式或添加新数据库列,则此类将需要更改。
  • 该类与数据库驱动程序紧密耦合。数据库驱动程序或 SQL 查询中的任何更改都将导致此类的修改。
  • 交易的格式不能单独测试,因为它没有被 BankStatementMgr公开。
  • 代码不是模块化的,因为多个功能交织在一起。

可以通过以下方法解决上面的问题:

  • 定义一个单独的格式化程序,其职责是格式化交易。
  • 添加一个数据库访问对象或 DAO,它将封装数据库驱动程序并完成所有繁重的查询工作。
  • BankStatementMgr会将请求委托给 DAO 获取数据,然后将响应传递给格式化程序进行美化。
  • 通过这种方式,我们可以隔离测试DAO和Formatter,实现松耦合。因此,它将通过分离职责使代码模块化

以下是我们修改后的代码:

文章图片2

银行报表管理器

文章图片3

语句格式器

文章图片4

事务DAO

所以每次在一个类中添加一个方法、或者一个方法中添加代码逻辑的时候,问问自己这真的属于这里吗?这是我家的“孩子”吗?

O—开闭原则

该原则指出代码应该对扩展开放,对修改关闭。如果需要添加新功能,则必须扩展该类。此外,为了使系统具有可扩展性,它的行为应该被隔离。

我们将通过一个例子来理解这一点。假设您是电子商务商户,通过不同的方式接受付款。您整合了Paypal、Wepay、Google Pay等不同模式,开发了支付处理器。你写出了下面的代码:

文章图片5

支付处理器

文章图片6

付款处理程序

处理付款请求的PaymentHandler 。PaymentProcessor确定模式并将其委托给正确的操作。此代码违反了开闭原则,因为任何功能都需要在PaymentProcessor和PaymentHandler 中进行修改。这种设计是不可扩展的,因为每一种新的支付方式都会在 switch 语句中引入一个新的 case 块。

为了使代码可扩展,我们可以使PaymentHandler抽象并定义一个方法来处理付款。为了处理新的支付模式,我们可以扩展这个基类并覆盖它的 handlePayment 方法。下面是新代码。

文章图片7

抽象支付处理程序

文章图片8

谷歌支付处理程序

文章图片9

CardPaymentHandler

我们现在将创建一个工厂类,它将负责存储特定的处理程序并根据模式返回它。

文章图片10

支付处理器工厂

文章图片11

支付处理器

我们的新代码现在符合开闭原则。要添加新的行为,我们只需要扩展我们的抽象类PaymentHandler并在工厂中配置它。无需修改 PaymentProcessor。

L—里式替换原则

乍一看,这个名字听起来很吓人。该原则指出,同一超类的对象应该能够在不破坏现有代码的情况下相互替换。

我们将以开发电影剪贴板为例。scrapper 提供了一个通过电影名称或演员搜索电影的界面。

文章图片12

电影搜索

文章图片13

电影数据库搜索

文章图片14

烂番茄搜索

文章图片15

使用 MovieSearch 接口的客户端代码

我们有两种不同的实现。一个用于烂番茄,另一个用于 IMDB。它们都是可替换的,并且可以使用相同的界面进行访问。

如果未实现派生类中的方法,则违反了该原则。下面是一个违反里氏原则的例子。

文章图片16

所有电影搜索

在这种情况下,我们不能用 All Movies 替换其他派生类,例如 IMDB 和 烂番茄。方法searchByMovieName不是由它实现的,并且不会导致客户端代码中的一致行为。

I—接口隔离原则

根据这个原则,客户端不应该实现它不需要的方法。如果您定义客户端不使用的方法,接口会变得过于笨重和受到污染。

如果一个界面由于混合功能而变得太大,将它分成多个较小的界面是有意义的。让我们看一个投资组合服务示例,该服务允许客户订购股票、ETF、期权等。

文章图片17

接口组合

我们已经定义了一个接口Portfolio,它允许客户订购股票、ETF 以及两者的组合。

文章图片18

ETF订单服务

文章图片19

股票订单服务

我们有两种不同的投资组合服务实现。StockOrderService尚未实现orderETF和orderStockAndETFs方法。ETFOrderService仅仅实现orderETF。

如果我们决定在订购股票时添加价格作为参数怎么办?它需要更改orderStocks方法以接受价格作为参数。此外,此更改必须由ETFOrderService 合并,即使它不支持orderStocks方法。

为了克服这个问题,我们可以将接口分为两个StockPortfolio,ETFPortfolio。

文章图片20

股票投资组合

文章图片21

ETF投资组合

使用新接口,StockOrderService 不需要处理订购 ETF。这同样适用于 ETFOrderService。

文章图片22

ETF订单服务

文章图片23

股票订单服务

接口隔离与单一职责和里氏替换原则有一些相似之处。

在上面带有庞大接口的示例中,我们在 StockOrderService 中抛出了一个异常。这违反了里氏替换原则。在这种情况下,派生类不会扩展功能。

如果在接口中定义了不相关的方法,那么该类将有多种更改原因。这违反了单一职责原则。

D—依赖倒置

根据依赖倒置,程序中的高层模块不能与低层模块紧密耦合。两个模块都必须依赖于抽象。该原则提供了一种构建松耦合软件模块的机制。

让我们看看下面的例子。在此示例中,类OrderHistory从 PostgreSQL 数据存储中获取数据。

文章图片24

订单历史

OrderHistory类必须知道 PostgresDB 依赖项的实现细节。如果我们决定使用不同的数据库驱动程序,我们将需要用新的依赖项替换所有PostgresDB实例。

此外,数据库驱动程序更改的功能之一是什么?它还需要更改调用数据库驱动程序方法的OrderHistory类。

可以通过声明接口DataStore来消除这种耦合。该接口将公开消费者将调用的 API。我们可以有多个DataStore实现— Postgres DataStore 、MySQL DataStore 等

文章图片25

数据存储

文章图片26

Postgres数据存储

文章图片27

订单历史

我们的消费者类现在不必处理正在使用的数据存储的底层细节。高级模块OrderHistory依赖于接口 DataStore 来访问数据。较低级别DataStore实现中的任何更改都不会对OrderHistory产生任何影响。

此外,由于模块是松散耦合的,因此可以独立测试它们。可以使用依赖注入轻松地将新实现注入到高级模块中。

这也就是我们经常说的要面向接口变成,而非实现,因为接口意味着契约,更加稳定。

总结

以上五项原则构成了软件工程中遵循的最佳实践的基石。在日常工作中实践上述原则有助于提高软件的可读性、模块化、可扩展性和可测试性。

最终,它有助于构建易于理解且维护良好的软件。遵循上述做法有助于提高开发人员的工作效率和工程团队的敏捷性。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多