核心要点
如果要对每个 SOLID 原则的相关性做出武断的“所罗门式”决定,我会说依赖倒置原则 (DIP) 是最被低估的原则。虽然面向对象设计领域的一些核心概念一开始很难理解,例如关注点分离和实现切换,但另一方面,更直观、更清晰的范例则更简单,例如面向接口编程。不幸的是,DIP 的正式定义笼罩着双刃剑般的诅咒/祝福,这往往使程序员忽略它,因为在许多情况下,人们默认认为该原则只不过是前面提到的“面向接口编程”戒律的另一种说法:
乍一看,上述陈述似乎不言自明。考虑到目前没有人会不同意建立在对具体实现的强烈依赖之上的系统是不良设计的凶兆,因此切换一些抽象是完全合理的。因此,这将使我们回到起点,认为 DIP 的主要关注点是关于面向接口编程。实际上,在满足该原则的要求时,将接口与实现解耦只是一个半成品的方法。缺少的部分是实现真正的反转过程。当然,由此产生的问题是:什么东西的反转?传统意义上,系统总是设计为让高层组件(无论是类还是过程例程)依赖于低层组件(细节)。例如,日志记录模块可能对一系列具体的日志记录器(实际上将信息记录到系统中)具有很强的依赖性。因此,每当日志记录器的协议被修改时,这个方案都会将副作用嘈杂地向上层传递,即使该协议已经被抽象出来。然而,DIP 的实现有助于在一定程度上减轻这些涟漪,方法是让日志记录模块拥有协议,从而反转整体依赖关系的流程。反转后,日志记录器应该忠实地遵守协议,因此如果将来发生变化,它们应该相应地进行更改并适应协议的波动。简而言之,这表明 DIP 在幕后比仅仅依赖于标准接口-实现解耦带来的大量好处要复杂一些。是的,它讨论了让高层和低层模块都依赖于抽象,但同时高层模块必须拥有这些抽象——这是一个微妙但相关的细节,不能轻易忽略。正如您可能预期的那样,一种可能帮助您更容易理解 DIP 实际涵盖内容的方法是通过一些实践代码示例。因此,在本文中,我将设置一些示例,以便您可以学习如何在开发 PHP 应用程序时充分利用此 SOLID 原则。
开发一个简单的存储模块(DIP 中缺失的“I”)
许多开发人员,特别是那些厌恶面向对象 PHP 的冰冷水域的开发人员,倾向于将 DIP 和其他 SOLID 原则视为僵化的教条,这与该语言固有的实用主义背道而驰。我可以理解这种想法,因为在野外很难找到展示该原则真正好处的实用 PHP 示例。我不是想把自己吹捧成一位开明的程序员(那套西装不太合身),但为了一个好的目标而努力并从实践的角度演示如何在现实用例中实现 DIP 还是很有用的。首先,考虑一下简单文件存储模块的实现。该模块负责读取和写入指定目标文件的数据。在非常简化的层面,问题中的模块可以这样编写:
<?php namespace LibraryEncoderStrategy; class Serializer implements Serializable { protected $unserializeCallback; public function __construct($unserializeCallback = false) { $this->unserializeCallback = (boolean) $unserializeCallback; } public function getUnserializeCallback() { return $this->unserializeCallback; } public function serialize($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function unserialize($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if ($this->unserializeCallback) { $callback = ini_get("unserialize_callback_func"); if (!function_exists($callback)) { throw new BadFunctionCallException( "The php.ini unserialize callback function is invalid."); } } if (($data = @unserialize($data)) === false) { throw new RuntimeException( "Unable to unserialize the supplied data."); } return $data; } }
<?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $serializer; protected $file; public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) { $this->serializer = $serializer; $this->setFile($file); } public function getSerializer() { return $this->serializer; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->serializer->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->serializer->unserialize( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }
该模块是一个相当简单的结构,仅由几个基本组件组成。第一个类读取和写入文件系统中的数据,第二个类是一个简单的 PHP 序列化器,用于在内部生成数据的可存储表示。这些示例组件很好地在隔离状态下执行其业务,并且可以像这样连接在一起以同步工作:
<?php use LibraryLoaderAutoloader, LibraryEncoderStrategySerializer, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $fileStorage = new FileStorage(new Serializer); $fileStorage->write(new stdClass()); print_r($fileStorage->read()); $fileStorage->write(array("This", "is", "a", "sample", "array")); print_r($fileStorage->read()); $fileStorage->write("This is a sample string."); echo $fileStorage->read();
乍一看,考虑到该模块的功能允许从文件系统中轻松保存和获取各种数据,因此该模块表现出相当不错的行为。此外,FileStorage 类在构造函数中注入了一个 Serializable 接口,因此依赖于抽象提供的灵活性,而不是僵化的具体实现。凭借这些优势,该模块有什么问题呢?通常情况下,肤浅的第一印象可能会很棘手且模糊。仔细观察一下,不仅 FileStorage 实际上依赖于序列化器,而且由于这种紧密依赖,从目标文件存储和提取数据仅限于使用 PHP 的原生序列化机制。如果数据必须作为 XML 或 JSON 传递给外部服务会发生什么情况?精心设计的模块不再可重用。令人悲伤但真实!这种情况提出了一些有趣的问题。首先也是最重要的一点是,即使使它们相互操作的协议已经与实现隔离,FileStorage 仍然表现出对低层 Serializer 的强烈依赖。其次,问题中协议公开的通用性级别非常有限,仅限于将一个序列化器换成另一个序列化器。在这种情况下,依赖于抽象是一种虚幻的感知,而且 DIP 鼓励的真正反转过程从未实现。可以重构文件模块的某些部分,使其忠实地遵守 DIP 的要求。这样做,FileStorage 类将获得用于存储和提取文件数据的协议的所有权,从而摆脱对低层序列化器的依赖,并使您能够在运行时在多个存储策略之间切换。这样做,您实际上将免费获得很大的灵活性。因此,让我们继续前进,看看如何将文件存储模块转换为真正符合 DIP 的结构。
反转协议所有权和解耦接口与实现(充分利用 DIP)
虽然没有很多选择,但仍有一些方法可以有效地反转 FileStorage 类及其低层协作者之间的协议所有权,同时保持协议的抽象性。但是,有一种方法非常直观,因为它依赖于 PHP 命名空间开箱即用的自然封装。为了将这个有点难以捉摸的概念转化为具体的代码,应该对模块进行的第一个更改是定义一个更宽松的协议来保存和检索文件数据,这样就可以轻松地通过除 PHP 序列化之外的其他格式来操作它。如下所示的一个精简的、隔离的接口可以优雅而简单地完成这项工作:
<?php namespace LibraryEncoderStrategy; class Serializer implements Serializable { protected $unserializeCallback; public function __construct($unserializeCallback = false) { $this->unserializeCallback = (boolean) $unserializeCallback; } public function getUnserializeCallback() { return $this->unserializeCallback; } public function serialize($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function unserialize($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if ($this->unserializeCallback) { $callback = ini_get("unserialize_callback_func"); if (!function_exists($callback)) { throw new BadFunctionCallException( "The php.ini unserialize callback function is invalid."); } } if (($data = @unserialize($data)) === false) { throw new RuntimeException( "Unable to unserialize the supplied data."); } return $data; } }
EncoderInterface 的存在似乎对文件模块的整体设计没有产生深远的影响,但它的作用远不止表面承诺的那些。第一个改进是定义了一个高度通用的协议来编码和解码数据。第二个改进与第一个同样重要,那就是现在协议的所有权属于 FileStorage 类,因为该接口存在于该类的命名空间中。简而言之,我们仅仅通过编写一个正确命名空间的接口,就设法使仍然未定义的低层编码器/解码器依赖于高层 FileStorage。简而言之,这就是 DIP 在其学术面纱背后所推崇的实际反转过程。当然,如果 FileStorage 类没有被修改为注入前面接口的实现者,那么反转将是一个笨拙的半途而废的尝试,因此以下是重构后的版本:
<?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $serializer; protected $file; public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) { $this->serializer = $serializer; $this->setFile($file); } public function getSerializer() { return $this->serializer; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->serializer->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->serializer->unserialize( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }
现在 FileStorage 在构造函数中明确声明了编码/解码协议的所有权,唯一剩下的事情就是创建一组具体的低层编码器/解码器,从而允许您处理多种格式的文件数据。这些组件中的第一个只是之前编写的 PHP 序列化器的重构实现:
<?php use LibraryLoaderAutoloader, LibraryEncoderStrategySerializer, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $fileStorage = new FileStorage(new Serializer); $fileStorage->write(new stdClass()); print_r($fileStorage->read()); $fileStorage->write(array("This", "is", "a", "sample", "array")); print_r($fileStorage->read()); $fileStorage->write("This is a sample string."); echo $fileStorage->read();
剖析 Serializer 背后的逻辑肯定是多余的。尽管如此,值得指出的是,它现在不仅依赖于更宽松的编码/解码抽象,而且抽象的所有权在命名空间级别明确公开。同样,我们可以更进一步,着手编写更多编码器,以便突出 DIP 带来的好处。话虽如此,以下是另一个额外的低层组件的编写方式:
<?php namespace LibraryFile; interface EncoderInterface { public function encode($data); public function decode($data); }
正如预期的那样,额外编码器背后的底层逻辑通常类似于第一个 PHP 序列化器,除了任何明显的改进和变体。此外,这些组件符合 DIP 强加的要求,因此遵守 FileStorage 命名空间中定义的编码/解码协议。由于文件模块中的高层和低层组件都依赖于抽象,并且编码器对文件存储类具有明确的依赖性,因此我们可以安全地声称该模块的行为符合 DIP 规范。此外,以下示例展示了如何将这些组件组合在一起:
<?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $encoder; protected $file; public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) { $this->encoder = $encoder; $this->setFile($file); } public function getEncoder() { return $this->encoder; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->encoder->encode($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->encoder->decode( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }
除了模块向客户端代码公开的一些简单的细微之处外,它对于说明要点以及以相当有指导意义的方式演示为什么 DIP 的谓词实际上比旧的“面向接口编程”范例更广泛非常有用。它描述并明确规定了依赖项的反转,因此应该通过不同的机制来实现。PHP 的命名空间是一种在没有过多负担的情况下实现此目的的好方法,尽管像定义结构良好、表达性强的应用程序布局这样的传统方法也可以产生相同的结果。
结束语
通常情况下,基于主观专业知识的观点往往带有偏见,当然,我在本文开头表达的观点也不例外。然而,确实存在一种轻微的倾向,即为了其更复杂的 SOLID 对应物而忽略依赖倒置原则,因为它很容易误解为依赖抽象的同义词。此外,一些程序员倾向于直觉地反应,并将术语“反转”视为控制反转的缩写表达式,虽然两者彼此相关,但这最终是一个错误的概念。既然您知道了 DIP 的真正内涵,请务必利用它带来的所有好处,这肯定会使您的应用程序不易受到随着时间的推移而可能出现的脆弱性和僵硬性问题的影响。图片来自 kentoh/Shutterstock
关于依赖倒置原则的常见问题
依赖倒置原则 (DIP) 是面向对象编程中 SOLID 原则的一个关键方面。其主要目的是解耦软件模块。这意味着提供复杂逻辑的高层模块与提供基本操作的低层模块分离。通过这样做,对低层模块的更改将对高层模块的影响最小,从而使整个系统更易于管理和维护。
传统的程序化编程通常涉及高层模块依赖于低层模块。这可能导致一个僵化的系统,其中一个模块的更改会对其他模块产生重大影响。另一方面,DIP 反转了这种依赖关系。高层和低层模块都依赖于抽象,这促进了灵活性,并使系统更能适应变化。
当然,让我们考虑一个从文件读取数据并处理数据的简单程序示例。在传统方法中,处理模块可能直接依赖于文件读取模块。但是,使用 DIP,这两个模块都将依赖于一个抽象,例如“DataReader”接口。这意味着处理模块没有直接绑定到文件读取模块,我们可以轻松切换到不同的数据源(例如数据库或 Web 服务),而无需更改处理模块。
DIP 可以为您的代码带来多项好处。它促进了解耦,这使得您的系统更灵活,更容易修改。它还提高了代码的可测试性,因为依赖关系可以轻松地被模拟或存根。此外,它鼓励良好的设计实践,例如面向接口编程而不是面向实现编程。
虽然 DIP 具有许多优点,但它也可能引入复杂性,尤其是在大型系统中,抽象的数量可能变得难以管理。它也可能导致编写更多代码,因为您需要定义接口并可能创建其他类来实现这些接口。但是,这些挑战可以通过良好的设计和架构实践来减轻。
DIP 是 SOLID 首字母缩写中的最后一个原则,但它与其他原则密切相关。例如,单一职责原则 (SRP) 和开闭原则 (OCP) 都促进了解耦,这是 DIP 的一个关键方面。里氏替换原则 (LSP) 和接口隔离原则 (ISP) 都处理抽象,而抽象是 DIP 的核心。
绝对可以。虽然 DIP 通常是在 Java 和其他面向对象语言的上下文中讨论的,但该原则本身是与语言无关的。您可以在任何支持抽象的语言中应用 DIP,例如接口或抽象类。
一个好的起点是在您的代码中查找高层模块直接依赖于低层模块的区域。考虑一下您是否可以在这些模块之间引入抽象来解耦它们。请记住,目标不是消除所有直接依赖关系,而是确保依赖关系是针对抽象的,而不是针对具体的实现。
DIP 主要用于改进代码的结构和可维护性,而不是其性能。但是,通过使您的代码更模块化且更容易理解,它可以帮助您更有效地识别和解决性能瓶颈。
虽然 DIP 的好处在大型复杂系统中往往更明显,但它在较小的项目中也可能有用。即使在小型代码库中,解耦模块也可以使您的代码更容易理解、测试和修改。
以上是依赖性反转原理的详细内容。更多信息请关注PHP中文网其他相关文章!