单一职责 (SRP)、开放/封闭 (OCP)、里氏替换、接口隔离和依赖倒置。每次编写代码时都应指导您的五个敏捷原则。
告诉您任何一项 SOLID 原则比另一项更重要是不公平的。然而,可能没有一个比依赖倒置原则(简称 DIP)对您的代码产生如此直接和深远的影响了。如果您发现其他原则难以掌握或应用,请从这一原则开始,然后将其余原则应用于已经遵循 DIP 的代码。
定义
A.高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
B. 抽象不应依赖于细节。细节应取决于抽象。
这个原则由 Robert C. Martin 在他的《敏捷软件开发、原则、模式和实践》一书中定义,后来在《C# 中的敏捷原则、模式和实践》一书中重新发布了 C# 版本,这是最后一个原则五个 SOLID 敏捷原则。
现实世界中的 DIP
在我们开始编码之前,我想给你讲一个故事。在Syneto,我们并不总是对我们的代码那么小心。几年前,我们所知甚少,尽管我们尽力做到最好,但并非所有项目都那么好。我们经历了地狱又回来了,我们通过尝试和错误学到了很多东西。
Bob 叔叔(Robert C. Martin)的 SOLID 原则和简洁架构原则改变了我们的游戏规则,并以难以描述的方式改变了我们的编码方式。简而言之,我将尝试举例说明 DIP 实施的一些对我们的项目产生重大影响的关键架构决策。
大多数 Web 项目包含三种主要技术:HTML、PHP 和 SQL。我们正在讨论的这些应用程序的特定版本或您使用的 SQL 实现类型都无关紧要。问题是,来自 HTML 表单的信息必须以某种方式最终进入数据库。两者之间的粘合剂可以由 PHP 提供。
最重要的是,这三种技术很好地代表了三个不同的架构层:用户界面、业务逻辑和持久性。我们稍后将讨论这些层的含义。现在,让我们关注一些奇怪但经常遇到的解决方案,以使这些技术协同工作。
我多次看到一些项目在 HTML 文件内的 PHP 标记中使用 SQL 代码,或者 PHP 代码回显 HTML 页面并直接解释 $_GET
或 $_POST
全局变量。但为什么这样不好呢?
上面的图像代表了我们在上一段中描述的原始版本。箭头代表各种依赖关系,我们可以得出结论,基本上一切都依赖于一切。如果我们需要更改数据库表,我们最终可能会编辑 HTML 文件。或者,如果我们更改 HTML 中的字段,最终可能会更改 SQL 语句中的列名称。或者,如果我们看第二个模式,如果 HTML 发生变化,我们很可能需要修改 PHP,或者在非常糟糕的情况下,当我们从 PHP 文件内部生成所有 HTML 内容时,我们肯定需要将 PHP 文件更改为修改 HTML 内容。因此,毫无疑问,类和模块之间的依赖关系是曲折的。但事情并没有就此结束。可以存储程序; SQL 表中的 PHP 代码。
在上面的架构中,对 SQL 数据库的查询返回使用表中的数据生成的 PHP 代码。这些 PHP 函数或类正在执行其他 SQL 查询,这些查询返回不同的 PHP 代码,并且循环继续,直到最终获取并返回所有信息...可能返回到 UI。
我知道这对你们许多人来说可能听起来很离谱,但如果您还没有参与过以这种方式发明和实施的项目,那么您在未来的职业生涯中肯定会这样做。大多数现有项目,无论使用何种编程语言,都是根据旧原则编写的,由那些不关心或不知道做得更好的程序员编写。如果您正在阅读这些教程,那么您很可能已经达到了更高的水平。您已经准备好,或者准备好尊重您的职业,拥抱您的技艺,并做得更好。
另一种选择是重复前人所犯的错误并承担后果。在Syneto,当我们的一个项目由于其旧的和相互依赖的架构而达到几乎无法维护的状态并且我们基本上不得不永远放弃它时,我们决定不再走那条路。从那时起,我们一直努力拥有一个干净的架构,正确尊重 SOLID 原则,最重要的是依赖倒置原则。
这个架构的神奇之处在于依赖关系是如何指向的:
- 用户界面(在大多数情况下是 Web MVC 框架)或项目的任何其他交付机制将取决于业务逻辑。业务逻辑是相当抽象的。用户界面非常具体。 UI只是项目的一个细节,而且变化很大。任何东西都不应该依赖于 UI,任何东西都不应该依赖于您的 MVC 框架。
- 我们可以做的另一个有趣的观察是,持久性、数据库、MySQL 或 PostgreSQL,取决于业务逻辑。您的业务逻辑与数据库无关。这允许根据您的意愿交换持久性。如果明天您想将 MySQL 更改为 PostgreSQL 或只是纯文本文件,您可以这样做。当然,您需要为新的持久化方法实现特定的持久化层,但您不需要修改业务逻辑中的一行代码。向持久层演进教程中有关于持久性主题的更详细说明。
- 最后,在业务逻辑的右侧,在其外部,我们拥有创建业务逻辑类的所有类。这些是由我们应用程序的入口点创建的工厂和类。许多人倾向于认为这些属于业务逻辑,但是当他们创建业务对象时,他们唯一的原因就是这样做。它们只是帮助我们创建其他类的类。它们提供的业务对象和逻辑独立于这些工厂。我们可以使用不同的模式,如简单工厂、抽象工厂、构建器或简单的对象创建来提供业务逻辑。没关系。一旦创建了业务对象,它们就可以完成自己的工作。
显示代码
如果您尊重经典的敏捷设计模式,那么在架构级别应用依赖倒置原则 (DIP) 是非常容易的。在业务逻辑中练习和举例也很容易,甚至很有趣。我们将想象一个电子书阅读器应用程序。
class Test extends PHPUnit_Framework_TestCase {
function testItCanReadAPDFBook() {
$b = new PDFBook();
$r = new PDFReader($b);
$this->assertRegExp('/pdf book/', $r->read());
}
}
class PDFReader {
private $book;
function __construct(PDFBook $book) {
$this->book = $book;
}
function read() {
return $this->book->read();
}
}
class PDFBook {
function read() {
return "reading a pdf book.";
}
}
登录后复制
我们开始将电子阅读器开发为 PDF 阅读器。到目前为止,一切都很好。我们有一个使用 PDFBook
的 PDFReader
类。读者上的 read()
函数委托给本书的 read()
方法。我们只是通过在 PDFBook
的 reader()
方法返回的字符串的关键部分之后进行正则表达式检查来验证这一点。
请记住,这只是一个示例。我们不会实现PDF文件或其他文件格式的阅读逻辑。这就是为什么我们的测试将仅检查一些基本字符串。如果我们要编写真正的应用程序,唯一的区别是我们如何测试不同的文件格式。依赖结构与我们的示例非常相似。
拥有一个使用 PDF 书籍的 PDF 阅读器对于有限的应用程序来说可能是一个合理的解决方案。如果我们的范围是编写一个 PDF 阅读器,仅此而已,那么它实际上是一个可以接受的解决方案。但我们想编写一个通用的电子书阅读器,支持多种格式,其中我们第一个实现的版本是 PDF。让我们重命名我们的读者类。
class Test extends PHPUnit_Framework_TestCase {
function testItCanReadAPDFBook() {
$b = new PDFBook();
$r = new EBookReader($b);
$this->assertRegExp('/pdf book/', $r->read());
}
}
class EBookReader {
private $book;
function __construct(PDFBook $book) {
$this->book = $book;
}
function read() {
return $this->book->read();
}
}
class PDFBook {
function read() {
return "reading a pdf book.";
}
}
登录后复制
重命名没有功能上的反作用。测试仍在通过。
测试于下午 1:04 开始...
PHPUnit 3.7.28 由 Sebastian Bergmann 编写。
时间:13 毫秒,内存:2.50Mb
好的(1 个测试,1 个断言)
进程已完成,退出代码为 0
但它具有严重的设计效果。
我们的读者变得更加抽象。更一般。我们有一个通用的 EBookReader
,它使用非常具体的书籍类型 PDFBook
。抽象取决于细节。我们的书是 PDF 类型这一事实应该只是一个细节,任何人都不应该依赖它。
class Test extends PHPUnit_Framework_TestCase {
function testItCanReadAPDFBook() {
$b = new PDFBook();
$r = new EBookReader($b);
$this->assertRegExp('/pdf book/', $r->read());
}
}
interface EBook {
function read();
}
class EBookReader {
private $book;
function __construct(EBook $book) {
$this->book = $book;
}
function read() {
return $this->book->read();
}
}
class PDFBook implements EBook{
function read() {
return "reading a pdf book.";
}
}
登录后复制
反转依赖关系最常见、最常用的解决方案是在我们的设计中引入一个更抽象的模块。 “OOP 中最抽象的元素是接口。因此,任何其他类都可以依赖于接口并且仍然遵循 DIP”。
我们为读者创建了一个界面。该接口名为 EBook
,代表 EBookReader
的需求。这是尊重接口隔离原则 (ISP) 的直接结果,该原则提倡接口应反映客户端需求的理念。接口属于客户端,因此它们的命名反映了客户端需要的类型和对象,并且它们将包含客户端想要使用的方法。 EBookReader
使用 EBooks
并拥有 read()
方法是很自然的。
我们现在有两个依赖项,而不是单个依赖项。
- 第一个依赖项从
EBookReader
指向EBook
接口,并且它是类型用法。EBookReader
使用EBooks
。 - 第二个依赖项不同。它从
PDFBook
指向相同的EBook
接口,但它是类型实现。PDFBook
只是EBook
的一种特殊形式,因此实现了该接口来满足客户的需求。
不出所料,该解决方案还允许我们将不同类型的电子书插入阅读器。所有这些书籍的唯一条件是满足 EBook
接口并实现它。
class Test extends PHPUnit_Framework_TestCase {
function testItCanReadAPDFBook() {
$b = new PDFBook();
$r = new EBookReader($b);
$this->assertRegExp('/pdf book/', $r->read());
}
function testItCanReadAMobiBook() {
$b = new MobiBook();
$r = new EBookReader($b);
$this->assertRegExp('/mobi book/', $r->read());
}
}
interface EBook {
function read();
}
class EBookReader {
private $book;
function __construct(EBook $book) {
$this->book = $book;
}
function read() {
return $this->book->read();
}
}
class PDFBook implements EBook {
function read() {
return "reading a pdf book.";
}
}
class MobiBook implements EBook {
function read() {
return "reading a mobi book.";
}
}
登录后复制
这又将我们引向开闭原则,并且圆圈是闭合的。
依赖倒置原则是引导或帮助我们尊重所有其他原则的原则。尊重 DIP 将:
- 几乎迫使您遵守 OCP。
- 允许您分离职责。
- 让你正确使用子类型。
- 为您提供隔离界面的机会。
最终想法
就是这样。我们完了。所有有关 SOLID 原理的教程均已完成。对我个人来说,发现这些原则并在实施项目时牢记它们是一个巨大的变化。我完全改变了我对设计和架构的思考方式,我可以说从那时起我从事的所有项目都变得更加容易管理和理解。
我认为 SOLID 原则是面向对象设计最基本的概念之一。这些概念必须指导我们使我们的代码变得更好,并使我们作为程序员的生活变得更加轻松。设计良好的代码更容易让程序员理解。计算机很聪明,无论代码多么复杂,它们都可以理解。另一方面,人类能够在活跃、专注的头脑中保存的事物数量有限。更具体地说,此类事物的数量是神奇数字七、正负二。
我们应该努力让我们的代码围绕这些数字构建,有几种技术可以帮助我们做到这一点。函数长度最多为四行(五行包括定义行),以便它们可以同时适合我们的头脑。压痕深度未超过五层。方法不超过九个的类。通常使用五到九个类的设计模式。我们在上面的模式中的高层设计使用了四到五个概念。有五个 SOLID 原则,每个原则需要五到九个子概念/模块/类来举例说明。编程团队的理想规模是五到九人。公司中理想的团队数量是五到九个。
正如您所看到的,神奇的数字七、正负二就在我们身边,那么为什么您的代码应该有所不同呢?