SOLID 原则变得简单

2025-06-08

SOLID 原则变得简单

本文旨在对 SOLID 原则进行详尽的讲解,并深入探讨其优势以及应用过程中可能遇到的问题。让我们简要地介绍一下 SOLID 原则。

S — 单一职责原则 (SRP)
一个类应该有且仅有一个变更的理由。
当需求发生变化时,这意味着代码必须进行重构,也就是类必须进行修改。一个类的职责越多,它收到的变更请求就越多,这些变更的实现难度也就越大。类的职责彼此耦合,因为其中一个职责的变更可能会导致其他职责的变更,以便该类能够妥善处理其他职责。

  • 责任是什么?!

职责可以定义为变更的原因。每当我们认为代码的某些部分可能是一种职责时,就应该考虑将其与类分离。假设我们正在开发一个帮助人们在社区中更加活跃的项目,并且系统需要集成社交媒体。将社交媒体集成职责与系统的其他部分分离是一个好主意,因为我们应该始终做好应对外部变更的准备。
如果您想了解 SRP 的实际应用,请查看这篇关于 PORO(普通旧式 Ruby 对象)的文章,其中解释了如何在 Ruby on Rails 应用程序中实现 SRP,从而使代码库可扩展且易于维护。
您听说过某种东西同时开放和封闭吗?!我当然听说过,所以让我们一起来看看。O
— 开闭原则
您应该能够扩展类的行为,而无需修改它。
这一原则是构建可维护和可重用代码的基础。
罗伯特·C·马丁

事物如何才能既开放又封闭?!如果一个类满足以下两个标准,则它遵循 OCP 原则:
- 开放扩展,
这确保类的行为可以扩展。随着需求的变化,我们应该能够使类以新的、不同的方式运行,以满足新需求。-
封闭修改,
这类类的源代码是固定不变的,任何人都不允许对其进行修改。

  • 我们如何实现这一目标?

通过抽象。为了能够在不修改任何代码的情况下扩展类的行为,我们需要进行抽象。例如,如果我们有一个系统,它以不同的形状作为类来工作,我们可能会有像 Circle、Rectangle 这样的类。为了让依赖于这些类之一的类实现 OCP,我们需要引入一个 Shape 接口/类。然后,在需要依赖注入的地方,我们会注入一个 Shape 实例,而不是低级类的实例。这样,我们就可以在不修改依赖类的源代码的情况下添加新的形状。
那么,我们如何知道应该将 Shape 设置为类还是接口呢?为此,我们找到了里氏替换原则,它告诉我们何时使用继承是合适的。我们来看一下,好吗?

L — 里氏替换原则
派生类必须可以替换其基类。

这里需要的是类似以下替换属性:如果
对于每个类型 S 的对象 o1,都有一个类型 T 的对象 o2,使得对于所有
以 T 定义的程序 P,当 o1
替换为 o2 时,P 的行为不变,则 S 是 T 的子类型。Barbara
Liskov

让我们通过案例研究来形象化这个定义。假设我们有一个 Rectangle 类,还有一个扩展它的类 Square。假设 Rectangle 有两个方法,setWidth 和 setHeight,它们分别设置矩形的宽度和高度。
问题是 Rectangle 类和 Square 类中这两个方法的行为不同。原因在于,根据数学定义,Square 是高度和宽度相等的矩形。因此,这两个方法将更改相同的值,而对于 Rectangle,它们将分别更改宽度和高度,这两个值彼此不同。
当我们使用抽象(开放-封闭原则)时,我们希望方法在每个派生类中的行为相同,而不是不同。在本例中,我们可以清楚地看到,Square 类不应该扩展 Rectangle 类,因为继承的方法的行为不同。
解决方案
正如 Robert C. Martin 所建议的那样,我们应该遵循契约式设计。这意味着每个方法都应该定义先决条件和后置条件。先决条件必须成立才能执行方法,而后置条件在方法执行后也必须成立。

…在(衍生品中的)重新定义例程时,你只能
用较弱的条件替换其前置条件,用较强的条件替换其后置条件。
伯特兰·迈耶

这是我在网上找到的关于这个定义的清晰且切中要点的解释:
假设你的基类使用一个 int 成员。现在你的子类型要求该 int 为正数。这强化了先决条件,现在任何之前处理负数 int 时运行良好的代码都失效了。
同样,假设同样的场景,但基类用于保证成员在调用后为正数。然后子类型改变了行为,允许使用负数 int。之前在该对象上运行的代码(假设后置条件为正数 int)现在失效了,因为后置条件不成立。Robert
C. Martin 建议,记录(注释)每个方法的先决条件和后置条件会很有帮助。

I — 接口隔离原则
制作特定于客户端的细粒度接口。

客户端不应该被强迫实现他们不使用的接口。
罗伯特·C·马丁

换句话说,拥有许多更小的接口比拥有更少、更臃肿的接口更好。
例如,假设我们有一个名为 Animal 的接口,它包含 eat、sleep 和 walk 方法。这意味着我们有一个名为 Animal 的单体接口,但这并非完美的抽象,因为有些动物会飞。将这个单体接口根据角色拆分成更小的接口,我们将得到 CanEat、CanSleep 和 CanWalk 接口。这样,一个物种就能够进食、睡觉,例如飞翔。一个物种将是角色的组合,而不是被描述为一种动物,后者未必是最准确的描述。从更大规模来看,微服务是一个非常类似的情况,它们是按职责划分的系统组件,而不是一个庞大的单体。
通过拆分接口,我们倾向于组合而不是继承,倾向于解耦而不是耦合。我们倾向于通过按角色(职责)划分来组合,并通过避免在单体内部将派生类与不必要的职责耦合来解耦。

D — 依赖倒置原则
依赖于抽象,而不是具体。

A. 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。B
. 抽象不应该依赖于细节。细节应该依赖于抽象。
罗伯特·C·马丁

假设我们有一个系统,它通过外部服务(例如 Google、GitHub 等)处理身份验证。我们会为每种服务创建一个类:GoogleAuthenticationService、GitHubAuthenticationService 等。现在,假设系统中的某个地方需要对用户进行身份验证。为此,如上所述,我们提供了几种服务。为了能够使用所有服务,我们有两种选择:要么编写一段代码使每种服务适应身份验证过程,要么定义身份验证服务的抽象。第一种可能是一种肮脏的解决方案,将来可能会带来技术债务;如果要将新的身份验证服务集成到系统中,我们将需要更改代码,从而违反了 OCP。第二种可能性更清晰,它允许将来添加服务,并且可以在不更改集成逻辑的情况下对每个服务进行更改。通过定义 AuthenticationService 接口并在每个服务中实现它,我们就能够在身份验证逻辑中使用依赖注入,并使我们的身份验证方法签名类似于:authenticate(AuthenticationService authenticationService)。然后,我们可以通过特定的服务进行身份验证,如下所示:authenticate(new GoogleAuthenticationService)。这有助于我们概括身份验证逻辑,而无需单独集成每个服务。
通过依赖更高级别的抽象,我们可以轻松地将一个实例更改为另一个实例,从而更改其行为。依赖反转提高了代码的可重用性和灵活性。

遵循原则总是有好处
的。软件工程也不例外。遵循 SOLID 原则给我们带来了很多好处,它们使我们的系统可重用、可维护、可扩展、可测试等等。

要了解每项原则的更多优势,请务必阅读Bob 叔叔的文章。想更有趣、更简单地解释 SOLID 原则,请查看这些照片

如果您有任何问题、挑战性机会,或者只是想打个招呼,请随时给我发电子邮件(dhkelmendi@gmail.com )。

本文最初发表于HackerNoon

鏂囩珷鏉ユ簮锛�https://dev.to/dhurimkelmendi/solid-principles-made-easy-1pg
PREV
CSS 布局:从浮动到弹性框和网格的历史
NEXT
让你的 Linux 终端使用起来更愉快 简介 Git 别名:初学者 Git 别名:高级 致谢