当前位置: 首页 > 编程日记 > 正文

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

让我们从最后一个 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部分 。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。

相关文章:

李彦宏,韩寒等入围本年度《时代百人》候选名单

美国《时代》杂志周六(4月3日)公布了2010年度 “百位全球最具影响力人物”的200名候选人名单,其中中国最大网络搜索公司“百度”总裁李彦宏也以成功企业家入围候选人,同时入围的还有年仅27岁的80后作家韩寒。 其它“全球最具影响力人物”候选人名单中还包…

win10如何查看NVIDIA驱动的版本

入口 输入:控制面板 选择:硬件和声音 选择NVIDIA控制面板 点击小房子图标 看到版本是391.25

vb中5种打开文件夹浏览框的方法总结(转)

代码 众所周知,在vb中如果是打开某一个文件的话,非常简单,使用CommonDialog组件即可轻松完成,但是他只能选择文件,之后或许选取的文件路径,而如果想要浏览文件夹,就没这么方便了。这里介绍3个办…

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…

Ubuntu系统

1. Ubuntu 14.04 LTS安装 直接从官网下载Ubuntu14.04.2LTS http://www.ubuntu.com/download/desktop (你也可以下载最新的14.10---据说改变不大) 个人采用的是U盘安装,用了UltraISO这款软件(百度软件中心中便有---可以不破解试用来完成目的):具体流程: UltraISO上端文件打开,将…

win10下Anaconda如何查看PyTorch版本

以管理员身份打开Anaconda Powershell Prompt 按顺序输入以下三行命令即可

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

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

处理问题的方法--抽象和特例化

事实上我们在软件开发的过程中总是:遇到问题,解决问题,这么一个 简单的过程。处理一般类似问题的时候,我们经过抽象,有的提取算法,有的提取结构,有的提取流程等等,这样的过程可以简单…

121-Best Time to Buy and Sell Stock

题目: Say you have an array for which the ith element is the price of a given stock on day i. If you were only permitted to complete at most one transaction (ie, buy one and sell one share of the stock), design an algorithm to find the maximum p…

控制行输入以下两句命令16倍速播放青年大学习

//得到视频标签 playRate document.getElementsByTagName(video); //改变播放速率 playRate.Bvideo.playbackRate 16;

ios 8+ (xcode 6.0 +)应用程序Ad Hoc 发布前多设备测试流程详解

我们开发的程序在经过simulator以及自己的iOS设备测试后,也基本完成应用程序了,这时候我们就可以把它发布出去了更更多的人去测试,我们可以在iOS平台使用ad hoc实现。 你在苹果购买的开发者会员账号,允许100台设备和你的账号关联。…

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

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

让程序主窗口不显示在任务栏中

// 这样一句就能搞定了 在Form创建是调用 procedure TfrmWaitWindow.FormCreate(Sender: TObject); begin SetWindowLong(Application.Handle,GWL_EXSTYLE,WS_EX_TOOLWINDOW); end; 相关方法1 Application.Initialize; Application.CreateForm(TForm1, Form1); Application.S…

查缺补漏 | Python控制结构

1. if 表达式的简介写法 x if E else y 意思是如果条件表达式E成立,执行x,否则执行y 等价于 if E:x else:y 2. Python的while循环和其他语言相似(只是少了大括号),但是for循环区别大很多 for iter_var in iterable_object: sui…

Android学习——R文件丢失异常原因汇总

Console报错:R.java was modified manually! Reverting to generated version! 引言: R文件丢失异常在java开发中是个比较常见的异常,造成这个异常的原因可能非常微小,但是给Android开发者们造成的麻烦可是巨大的,当程序员们费尽千…

举个栗子看如何做MySQL 内核深度优化

2019独角兽企业重金招聘Python工程师标准>>> 本文由云社区发表 作者介绍:简怀兵,腾讯云数据库高级工程师,负责腾讯云CDB内核及基础设施建设;先后供职于Thomson Reuters和YY等公司,PTimeDB作者,曾…

Ubuntu--开启TELNET服务

1 sudo apt-get install xinetd telnetd 安装成功后,系统也会有相应提示, 测试安装完之后就可以Telnet,要是还不行继续 2 sudo vi /etc/inetd.conf 并加入以下一行 telnet stream tcp nowait telnetd /usr/sbin/tcpd /usr/sbin/in.telnetd …

Python的range()函数

如果想产生一个等差数列,用range()函数再合适不过。 range()函数可以有起始值、终值、步长三个参数。 range(start 0,end,step 1) 但是起始值和步长是可以缺省的。起始值的缺省值是0,步长的缺省值是1。 起始值被包含,终值不包含。 为了方…

C++链式继承

继承,对于学习C的每一个人来说,都不会陌生。在Qt的开发中,如果你需要对一个无边框的界面支持move操作,那么你就得通过继承重写虚函数来实现,这并不难,但如果我还需要对一个按钮支持移动,一般情况…

调度框架学习笔记(3)—— 集群调度框架的架构演进过程

本章是 The evolution of cluster scheduler architectures 文章的学习笔记。这篇文章讨论了这些年调度架构是如何发展的以及为什么会这样发展。 首先介绍一下这篇文章的作者:Malte Schwarzkopf,他目前在 MIT 的 PDOS实验室 作博士后,说起作者…

查缺补漏 | Python自定义函数

1 默认参数要放在自定义函数参数列表的最后,也就是说下面的定义是不允许的 2 调用函数时熟悉的是位置参数,但是也可以用关键字参数,也就是调用时把参数名写出来(可以通过它来改变参数的顺序)。不过貌似系统定义的函数不能用关键字参数&#x…

ZBar与ZXing使用后感觉

[原]ZBar与ZXing使用后感觉(上) 2014-3-18阅读2011 评论1 最近对二维码比较感兴趣,还是那句老话,那么我就对比了一下zxing和zbar 如果对于这两个的背景不了解的话,可以看我以前的文章,介绍了几个比较基础的…

X-UA-Compatible

X-UA-Compatible是IE8的一个专有<meta>属性&#xff0c;它告诉IE8采用何种IE版本去渲染网页&#xff0c;在html的<head>标签中使用。可以在微软官方文档获取更多介绍。 在IE8刚推出的时候&#xff0c;很多网页由于重构的问题&#xff0c;无法适应较高级的浏览器&a…

[转]【 视频 】PAR、DAR和SAR都是啥

原地址:http://blog.yikuyiku.com/?cat3 PAR —— Pixel Aspect Ratio 像素纵横比DAR —— Display Aspect Ratio 显示纵横比SAR —— Sample Aspect Ratio 采样纵横比 16&#xff1a;9和4&#xff1a;3指的是DAR&#xff0c;DAR和SAR之间没有必然联系。横向上的像素数目/纵向…

登高自卑 | 我的NumPy笔记

注&#xff1a;以下内容来自NumPy中文网 1 NumPy的矢量化和广播两个特征是大部分功能的基础。 矢量化让代码更接近标准的数学符号&#xff0c;更Pythonic&#xff0c;隐藏了所有的显示循环(幕后是C在做显示循环)。 广播即操作的隐式逐元素行为&#xff0c;不仅仅局限于算数运…

TI IPNC Web网页之网页修改教程

web网页程序修改 打开gStudio之后&#xff0c;点击菜单栏中Help->Contents。先把这个诡异的编程语言看一遍吧。这里搬一些东西出来。 GoDB简介 从第一副图片中&#xff0c;我们可以看出&#xff0c;从源文件到可执行文件的过程。 从第二幅图我们可以了解到GoDB是如何跨平台的…

Spring Cloud构建分布式电子商务平台:服务消费(基础)

使用LoadBalancerClient在Spring Cloud Commons中提供了大量的与服务治理相关的抽象接口&#xff0c;包括DiscoveryClient、这里我们即将介绍的LoadBalancerClient等。对于这些接口的定义我们在上一篇介绍服务注册与发现时已经说过&#xff0c;Spring Cloud做这一层抽象&#x…

OPENGL ES 对象的拾取

时间&#xff1a;19:51 2010-12-14 用户问题的说明 响应鼠标操作&#xff0c;其当中有一个非常重要的知识&#xff1a;使用鼠标点取&#xff0c;达到对三维模型对象的捕捉。 对象的拾取&#xff0c;这是3D当中的一个专业术语。也就是在二维屏幕当中选择三维对象。 我们要使用之…

如何禁止NumPy自动跳过数组的中心部分并仅打印角点

import numpy as np import sys np.set_printoptions(thresholdsys.maxsize)

用C#创建Windows服务(Windows Services)

转载自 hyslove最终编辑 hysloveWindows服务在Visual Studio 以前的版本中叫NT服务&#xff0c;在VS.net启用了新的名称。用Visual C# 创建Windows服务不是一件困难的事&#xff0c;本文就将指导你一步一步创建一个Windows服务并使用它。这个服务在启动和停止时&#xff0c;向一…