即便你是一个初级开发人员,你也可能据说过 SOLID 原则。它们无处不在。工做面试时,你也许听过这样的问题:“你是如何评估代码质量的呢?又是如何区分代码的好坏呢?”通常的答案相似这样:“我尽可能保持文件足够小。当文件变得很大时,我将移动部分代码到其余文件中。”。最糟糕的答案之一是:“我看到了就知道了。”当你用此类的描述回答以后,面试官一般会问:“你据说过 SOLID 原则吗?”。数据库
那么,什么是 SOLID 原则呢?它为何会被常常提到呢?维基百科 - SOLID 是这样描述的:“在面向对象的计算机编程中,术语 SOLID 是对五大软件设计原则的助记缩写,这五个原则旨在让软件设计更易理解、更灵活、更易于维护。它与 GRASP 软件设计原则并没有关系。这些软件设计原则是 Robert C. Martin 提出的众多原则中的一个子集。虽然它们适用于任何面向对象的设计,可是 SOLID 原则也能够造成诸如敏捷开发或者自适应软件开发等方法论的核心理念。Martin 在 2000年 的论文《设计原则与设计模式》中介绍了 SOLID 原则的理论。”。编程
若是正确地遵循了 SOLID 原则,那么它将引导你写出维护性和可测试性更好的高质量代码。若是需求变动,你也能够轻松地修改代码(与不遵照 SOLID 的代码相比)。我认为向你说起这些内容是很是重要的:这些原则关注的是可维护性、可测试性以及和人类思惟相关的更高质代码,而不是你为之编码的计算机。工做在以性能为主的领域的开发者各自有着不一样的编程方法。因为在咱们生活的世界里,人工时间成本比机器时间成本更昂贵,因此大多数开发者都不在高性能需求的领域里工做,并且他们经常被鼓励去使用面向对象的编程方法。在这种状况下,大部分开发者都能很好地应用 SOLID 原则。设计模式
从本文开始,我将按照单词首字母缩写的顺序尽力向你解释每个原则。为了简洁起见,我会把这个主题分为 3 个部分。本文是我将向你介绍的前两个原则的第 1 部分。好了,让咱们从字母 S 开始吧。服务器
SOLID 中的 “S” 表示的是单一职责原则(Single Responsibility Principle,简称 SRP),它是最容易理解也多是最容易让人忽视的一个原则。此原则意味着一个类应该只作且只能作一件事情。若是一个类未能达到它的目的,那么你就能够看着它并说“你作了一件事……”。网络
举例来讲,假如咱们须要从网络上获取一些 JSON 数据,而后解析它,并把结果保存在本地数据库中。根据咱们正在编码的平台,这种工做可使用为数很少的代码来实现。因为代码量很少,咱们可能会想把全部的逻辑所有扔到一个类中。可是,根据单一职责原则,这将会是一个糟糕的作法。咱们能够清楚地区分 3 个不一样的职责:从网络上获取 JSON 数据,解析数据,保存解析的结果到数据库中。基于此,咱们应该有 3 个类。模块化
第 1 个类应该只处理网络。咱们给它提供一个 URL,而后接收 JSON 数据或者在出现问题时,收到一个错误信息。函数
第 2 个类应该只解析它接收到的 JSON 数据并以相应的格式返回结果。post
第 3 个类应该以相应的格式接收 JSON 数据,并把它保存在本地数据库中。性能
为何非要这么麻烦呢?经过这样分离代码,咱们能得到什么好处呢?其中一个好处就是可测试性。对于网络请求类,咱们可使用一个测试的 URL 分别在请求成功和发生错误的测试用例下来观察它的正确行为。为了测试 JSON 模块,咱们能够提供一个模拟的 JSON 数据,而后查看它生成的正确数据。一样的测试原则也适用于数据库类(提供模拟数据,在模拟的数据库上测试结果)。
有了这些测试,若是咱们的程序出了问题,咱们能够运行测试并查看问题发生在哪一个地方。多是服务器上的某些部分发生了改变,致使咱们接收了有损数据。或者数据是正常的,可是咱们在 JSON 解析模块中遗漏了什么,导致咱们不能正确地解析数据。又或者可能咱们正尝试插入数据的数据库中不存在某个列。经过这些测试,咱们没必要猜想问题出在哪一个地方。看到了问题所在,咱们就努力地去解决它。
除了可测试性,咱们还拥抱了模块化。若是项目需求变动,服务器返回数据的格式是 XML 或者其余的自定义格式而非 JSON,那么咱们所要作的就是编写一个处理数据解析的新模块,而后用这个新的代替 JSON 模块。或者可能由于一些奇葩的理由,上述二者咱们都须要,而网络模块再根据一些规则调用正确的模块。
若是咱们有一些本地的 JSON 文件须要解析并将解析的数据发给其余模块时该怎么办呢?那么,咱们能够把本地的 JSON 发送给咱们的解析模块,而后获取结果并把它用在须要的地方。若是咱们须要以 Flat 的格式(译者注:Flat File)而不是数据库的形式本地保存数据呢?一样没问题,咱们能够用一个新的模块来替换数据库模块。
如你所见,这个看似简单的原则有不少优点。经过遵照这个原则,咱们已经可以想象的到咱们的代码库在可维护性方面会有重大改进。
字母“O”表示的是开闭原则( Open-Closed Principle,简称 OCP)。常言道,咱们的类应该对扩展开放,对修改关闭。什么意思呢?个人理解是,咱们应该以插件式的方式来编写类和模块。若是咱们须要额外的功能,咱们不该该修改类,而是可以嵌入一个提供这个额外功能的不一样类。为了解释个人理解,我将使用一个经典的计算器示例。这个计算器在最开始只能执行两种运算:加法和减法。计算器类看起来像下面这样(本文中的代码不是用特定语言编写的):
class Calculator {
public float add(float a, float b) {
return a + b
}
public float subtract(float a, float b) {
return a — b
}
}
复制代码
咱们像下面这样使用这个类:
Calculator calculator = new Calculator()
float sum = calculator.add(10, 2) //the value of sum is 12
float diff = calculator.subtract(10, 2) //the value of diff is 8
复制代码
如今,咱们假设客户但愿为这个计算器添加乘法功能。为了添加这个额外的功能,咱们必须编辑计算器类并添加乘法方法:
public float multiply(float a, float b) {
return a * b
}
复制代码
若是需求又一次改变,客户又须要除法,sin
,cos
,pow
以及众多的其余数学函数,咱们不得不一次又一次编辑这个类来添加这些需求。根据开闭原则,这并非一个明智的作法。由于这意味着咱们的类能够修改。咱们须要让它屏蔽修改,而对扩展开放,那么咱们该怎么作呢?
首先,咱们定义一个名为 Operation
的接口,这个接口只有一个名为 compute
的方法:
interface Operation {
float compute(float a, float b)
}
复制代码
以后,咱们能够经过实现 Operation
接口来建立操做类(本文中提供的大多数示例也能够经过继承和抽象类来完成,但我更喜欢使用接口)。为了重建简单的计算器示例,咱们将编写加法和减法类:
class Addition implements Operation {
public float compute(float a, float b) {
return a + b
}
}
class Subtraction implements Operation {
public float compute(float a, float b) {
return a — b
}
}
复制代码
咱们的新计算器类只有一个名叫 calculate
的方法,在这个方法中,咱们能够传递操做数与操做类:
class Calculator {
public float calculate(float a, float b, Operation operation) {
return operation.compute(a, b)
}
}
复制代码
咱们将像下面这样使用咱们的新类:
Calculator calculator = new Calculator()
Addition addition = new Addition()
Subtraction subtraction = new Subtraction()
float sum = calculator.calculate(10, 2, addition) //the value of sum is 12
float diff = calculator.calculate(10, 2, subtraction) //the value of diff is 8
复制代码
如今若是咱们须要添加乘法,咱们将建立这样的一个乘法运算类:
class Multiplication implements Operation {
public float compute(float a, float b) {
return a * b
}
}
复制代码
而后经过添加如下内容在上面的示例中使用它:
Multiplication multiplication = new Multiplication()
float prod = calculator.calculate(10, 2, multiplication) // the value of prod is 20
复制代码
咱们终于能够说咱们的计算器类对修改关闭,对扩展开放了。看一下这个简单的例子,你可能会说将这些额外的方法添加到原始的计算器类中也没什么大问题,还有就是可能更好的实现也就意味着编写更多的代码。诚然,在这个简单的情景中,我更赞同你的说法。可是,在现实生活里的复杂情景下,遵照开闭原则编码将大有裨益。也许你须要为每一个新功能添加远不止那三个方法,也许这些方法很是复杂。然而经过遵循开闭原则,咱们能够用不一样的类外化新的功能。它将有助于咱们以及他人更好地理解咱们的代码,而这主要是由于咱们必须专一于较小的代码块而不是滚动无休止的文件。
为了更好地可视化这个概念,咱们能够把计算器类视为第三方库的一部分,而且没法访问其源码。好的实现就是编写它的善良的人们遵照了开闭原则,使它对扩展开放。所以,咱们可使用本身的代码扩展其功能,并轻松地在咱们的项目中使用它。
若是这听起来仍让人犯傻,那就这样想吧:你刚刚为客户编写了一个很棒的软件,它完成了客户想要的一切。你尽最大能力编写了全部的内容,而且代码质量使人惊叹。数周后,客户想要新的功能。为了实现它们,你必须潜心投入到你的漂亮代码中,修改各类文件。这样作,有可能代码质量会受到影响,特别是在截止日期紧张时。若是你已经为你的代码编写了测试(这也是你应该作的),那么这些修改可能会破坏一些测试,你还必须修改这些测试。
这与遵照了开闭原则编写的代码造成了鲜明的对比。要实现新功能,你只需编写新代码便可。旧代码保持不变。你全部的旧测试仍然有效。由于咱们不是生活在一个完美的世界中,因此在某些跑偏的状况下,你可能仍然会对旧代码的某些部分进行细微的更改,但这与非开闭原则带来的修改相比则能够忽略不计。
除此以外,遵循开闭原则的编码方式还能让你在心理上得到极大的愉悦体验。其中一个就是,你只须要编写新代码,而无须为了实现新功能对你引觉得傲的代码痛下杀手。经过为新功能编写新代码,而不是修改旧代码,高涨的团队士气将随之而来。这能够提升生产效率,从而减小工做压力,改善生活质量。
我但愿你能看到这个原则的重要性。不过使人沮丧的是,在一个真实的项目中,主要是因为缺少魔法水晶球的能力,咱们很难预见将来以及应该如何、在哪里应用这个原则。可是,知道了开闭原则的确有助于在需求来临时识别出可能的用例。在一开始的实现中,当客户想要给这个计算器添加乘法和除法功能时,咱们随手将这两个方法添加到了 Calculator
类中。接下来,当他还要 sin
、cos
时,咱们也许会对本身说:“等会儿……”。等待事后,咱们开始重构代码以适配开闭原则来避免未来可能遇到的麻烦。如今,当客户还想要 tan
、pow
以及其余功能时,咱们早就搞定了。
……
你能够在 什么是SOLID原则(第2部分) 阅读下两个 SOLID 原则。若是你喜欢这篇文章,你能够在咱们的 官方站点 上找到更多信息。