SOLID: 第四部分 依赖倒置原则
单一职责 (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 书籍的 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."; } } 登录后复制
我们的读者变得更加抽象。更一般。我们有一个通用的 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."; } } 登录后复制
我们现在有两个依赖项,而不是单个依赖项。
- 第一个依赖项从
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."; } } 登录后复制