什么是SOLID原则(第3部分)

news/2024/9/9 14:23:52

让我们从最后一个 SOLID 原则开始吧,即依赖倒置原则(Dependency Inversion Principle,简称 DIP)(不要和依赖注入Dependency Injection ,DI 弄混淆了)。这个原则所说的是高级模块不应该依赖具象的低级模块,它们都应该依赖相应模块的抽象层。

图片描述

我仍将使用自行车的示例来尝试给你解释这个原则。首选看下这个 Bike 接口:

interface Bike {void pedal()void backPedal()
}

MountainBikeClassicBike 这两个类实现了上面的接口:

// 山地车
class MountainBike implements Bike {override void pedal() {// complex code that computes the inner workings of what happens // when pedalling on a mountain bike, which includes taking into // account the gear in which the bike currently is.}override void backPedal() {// complex code that computes what happens when we back pedal    // on a mountain bike, which is that you pedal in the wrong   // direction with no discernible effect on the bike}
}// 传统自行车
class ClassicBike implements Bike {override void pedal() {// the same as for the mountain bike with the distinction that // there is a single gear on a classic bike}override void backPedal() {// complex code that actually triggers the brake function on the     // bike}
}

正如你所看到的,踩脚踏板(pedal)会让自行车向前行驶,但是山地车 MountainBike 因为有多个齿轮,所以它的 pedal 会更加复杂。另外,向后踩脚踏板(back pedal)时,山地车不会做任何事,而传统自行车 ClassicBike 则会触发刹车操作。

图片描述

我之所以在每个方法的注释中都有提到“complex code”,是因为我想指出我们应该把上述代码移动到不同的模块中。我们这样做是为了简化自行车类以及遵循单一职责原则(自行车类不应该担起在你向前或向后踩脚踏板时究竟发生了什么的计算工作,它们应该处理有关自行车的更高级别的事情)。

为了做到这一点,我们将为每种类型的 pedalling 创建一些行为类。

class MountainBikePedalBehaviour {void pedal() {//complex code}
}class MountainBikeBackPedalBehaviour {void backPedal() {// complex code}
}class ClassicBikePedalBehaviour {void pedal() {// complex code}
}class ClassicBikeBackPedalBehaviour {void backPedal() {// complex code}
}

然后像下面这样使用这些类:

// 山地车
class MountainBike implements Bike {override void pedal() {var pedalBehaviour = new MountainBikePedalBehaviour()pedalBehaviour.pedal()}override void backPedal() {var backPedalBehaviour = new MountainBikeBackPedalBehaviour()backPedalBehaviour.backPedal()}
}// 传统自行车
class ClassicBike implements Bike {override void pedal() {var pedalBehaviour = new ClassicBikePedalBehaviour()pedalBehaviour.pedal()}override void backPedal() {var backPedalBehaviour = new ClassicBikeBackPedalBehaviour()backPedalBehaviour.backPedal()}
}

这个时候,我们可以很清楚地看到高级模块 MountainBike 依赖于某些具体的低级模块 MountainBikePedalBehaviourMountainBikeBackPedalBehaviourClassicBike 以及它的低级模块同样如此。根据依赖倒置原则,高级模块和低级模块都应该依赖抽象。为此,我们需要以下接口:

interface PedalBehaviour {void pedal()
}interface BackPedalBehaviour {void backPedal()
}

除了需要实现上面的接口外,行为类的代码与之前无异:

class MountainBikePedalBehaviour implements PedalBehaviour {override void pedal() {// same as before}
}

剩下的其他行为类同上。

图片描述

现在我们需要一种方法将 PedalBehaviourBackPedalBehaviour 传递给 MountainBikeClassicBike 类。我们可以选择在构造方法、pedal()pedalBack() 中完成这件事。本例中,我们使用构造方法。

class MountainBike implements Bike {PedalBehaviour pedalBehaviour;BackPedalBehaviour backPedalBehaviour;public MountainBike(PedalBehaviour pedalBehaviour,BackPedalBehaviour backPedalBehaviour) {this.pedalBehaviour = pedalBehaviour;this.backPedalBehaviour = backPedalBehaviour;}override void pedal() {pedalBehaviour.pedal();}override void backPedal() {backPedalBehaviour.backPedal();}
}

ClassicBike 类同上。

我们的高级模块(MountainBikeClassicBike)不再依赖于具体的低级模块,而是依赖于抽象的 PedalBehaviourBackPedalBehaviour

在我们的例子中,我们应用的主模块可能看起来向下面这样:

class MainModule {MountainBike mountainBike;ClassicBike classicBike;MountainBikePedalBehaviour mountainBikePedalBehaviour;ClassicBikePedalBehaviour classicBikePedalBehaviour;MountainBikeBackPedalBehaviour mountainBikeBackPedalBehaviour;ClassicBikeBackPedalBehaviour classicBikeBackPedalBehaviour;public MainModule() {mountainBikePedalBehaviour = new MountainBikePedalBehaviour();mountainBikeBackPedalBehaviour = new MountainBikeBackPedalBehaviour();mountainBike = new MountainBike(mountainBikePedalBehaviour,   mountainBikeBackPedalBehaviour);classicBikePedalBehaviour = new ClassicBikePedalBehaviour();classicBikeBackPedalBehaviour = new ClassicBikeBackPedalBehaviour();classicBike = new ClassicBike(classicBikePedalBehaviour,classicBikeBackPedalBehaviour);}public void pedalBikes() {mountainBike.pedal()classicBike.pedal()}public void backPedalBikes() {mountainBike.backPedal();classicBike.backPedal();}
}

可以看到,我们的 MainModule 依赖了具体的低级模块而不是抽象层。我们可以通过向构造方法中传递依赖来改善这种情况:

public MainModule(Bike mountainBike, Bike classicBike, PedalBehaviour mBikePB, BackPedalBehaviour mBikeBPB, PedalBehaviour cBikePB, BackPedalBehaviour cBikeBPB)...

现在,MainModule 部分依赖了抽象层,部分依赖了低级模块,这些低级模块也依赖了那些抽象层。所有这些模块之间的关系不再依赖于实现细节。

图片描述

在我们到达应用程序中的最高模块之前,为了尽可能地延迟一个具体类的实例化,我们通常要靠依赖注入和实现了依赖注入的框架。你可以在 这里 找到更多有关依赖注入的信息。我们可以将依赖注入视为帮助我们实现依赖倒置的工具。我们不断地向依赖链中传递依赖关系以避免具体类的实例化。

那么为什么要经历这一切呢?不依赖于具象的一个优点就是我们可以模拟一个类,从而使测试更容易进行。我们来看一个简单的例子。

interface Network {public String getServerResponse(URL serverURL);
}class NetworkRequestHandler implements Network {override public String getServerResponse(URL serverURL) {// network code implementation}
}

假设我们还有一个 NetworkManager 类,它有一个公共方法,通过使用一个 Network 的实例返回服务器响应:

public String getResponse(Network networkRequestHandler, URL url) {return networkRequestHandler.getServerResponse(url)
}

因为这样的代码结构,我们可以测试代码如何处理来自服务器的“404”响应。为此,我们将创 NetworkRequestHandler的模拟版本。我们之所以可以这么做,是因为 NetworkManager 依赖于抽象层,即 Network,而不是某个具体的 NetworkRequestHandler

class Mock404 implements Network {override public String getServerResponse(URL serverURL) {return "404"}
}

通过调用 getResponse 方法,传递 Mock404 类的实例,我们可以很容易地测试我们期望的行为。像 Mockito 这样的模拟库可以帮助你模拟某些类,而无需编写单独的类来执行此操作。

除了易于测试,我们的应用在多变情景下也能应对自如。因为模块之间的关系是基于抽象的,我们可以更改具体模块的实现,而无需大范围地更改代码。

最后同样重要的是这会让事情变得更简单。如果你有留意自行车的示例,你会发现 MountainBikeClassicBike 类非常相似。这就意味着我们不再需要单独的类了。我们可以创建一个简单的实现了 Bike 接口的类 GenericBike,然后山地车和传统自行车的实例化就像下面这样:

GenericBike mountainBike = new GenericBike(mbPedalB, mbBackPedalB);
GenericBike classicBike = new GenericBike(cbPedalB, cbBackPedalB);

我们减少了一半数量的具体自行车类的实现,这意味着我们的代码更容易管理。

总结

所有这些原则可能看起来有点矫枉过正,你可能会排斥它们。在很长的一段时间里,我和你一样。随着时间的推移,我开始逐渐把我的代码向增强可测试性和更易于维护的方向转变。渐渐地,我开始这样来思考事情:“如果只有一种方法可以把两个部分的内容分开,并将其放在不同的类中,以便我能……”。通常,答案是的确存在这样的一种方法,并且别人已经实现过了。大多数时候,这种方法都受到 SOLID 原则的启发。当然,紧迫的工期和其他现实生活中的因素可能会不允许你遵守所有这些原则。虽然很难 100% 实现 SOLID 原则,但是有比没有强吧。也许你可以尝试只在那些当需求变更时最容易受影响的部分遵守这些原则。你不必过分遵循它们,可以把这些原则视为你提高代码质量的指南。如果你不得不需要制作一个快速原型或者验证一个概念应用的可行性,那么你没有必要尽力去搭一个最佳架构。SOLID更像是一个长期策略,对于必须经得起时间考验的软件非常有用。

在这篇由三部分组成的文章中,我试图给你展示了一些有关 SOLID的我发现比较有趣的东西。关于 SOLID,还有很多看法和解释,为了更好地理解和从多个角度获取知识,请多阅读些其他文章。

我希望这篇文章对你有所帮助。

……

如果你还没有看过另两个部分,这里是它们的链接,第1部分 和 第2部分 。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。


http://lihuaxi.xjx100.cn/news/235698.html

相关文章

css盒模型

多个版本。 题目:谈谈你对CSS盒模型的认识 (1) 基本概念:标准模型IE模型 (2) 标准模型和IE模型区别 标准模型和IE模型的区别,就是宽度和高度的计算方式不同。 标准模型的宽度指的就是content的宽度,不包含padding和border。 IE模型…

R语言文摘:Subsetting Data

原文地址:https://www.statmethods.net/management/subset.html R has powerful indexing features for accessing object elements. These features can be used to select and exclude variables and observations. The following code snippets demonstrate ways…

树莓派安装go

简介 大学的时候在使用openfalcon的时候讲过这个东西,但是那时候是介绍open-falcon的,所以感觉不是很具体,所以今天在安装frp的时候也碰到了这个问题,我就具体的说下 安装go1.4 编译最新版本的go的时候一定要先编译安装go1.4&…

6年iOS开发程序员总结组件化—让你的项目一步到位

纯个人学习笔记分享, 不喜勿喷,自行取关! 技术不缺乏缔造者,网络不缺乏键盘侠,但缺乏分享技术的源动力! 近几年组件化大家吵的沸沸扬扬的,它其实也不是什么黄金圣衣,穿上立马让你的小宇宙提升几个档次,也不是海皇的三叉戟,入手就能…

vue-router点击切换路由报错

报错: 报错原因: 设置mode:history解决方法: 将router的mode设置为‘hash就不报错了 原因下次再分析?

JAVA 多用户商城系统b2b2c-Spring Cloud常见问题与总结(一)

在使用Spring Cloud的过程中,难免会遇到一些问题。所以对Spring Cloud的常用问题做一些总结。需要JAVA Spring Cloud大型企业分布式微服务云构建的B2B2C电子商务平台源码 一零三八七七四六二六 一、Eureka常见问题 1.1 Eureka 注册服务慢 默认情况下,服务…

SHELL训练营--day5__shell脚本(1)

shell脚本意义 shell是一种脚本语言,具备计算机语言的基本特点:逻辑判断、循环、自定义函数等。shell脚本 主要使用 linux系统的命令,来实现特定目的。可用于自动化运维,提长运维效率。 shell脚本基本结构和运行方法 shell脚本名字…

Swift中依赖注入的解耦策略

原文地址:Dependency Injection Strategies in Swift 简书地址:Swift中依赖注入的解耦策略 今天我们将深入研究Swift中的依赖注入,这是软件开发中最重要的技术之一,也是许多编程语言中使用频繁的概念。 具体来说,我们将…