依赖倒置原则的核心
If I ever had to make an ad-hoc “Solomonic” decision concerning the relevance of each SOLID principle, I would just dare say that the Dependency Inversion Principle (DIP) is the most underrated of all.
如果我不得不就每个SOLID原则的相关性做出临时的“ Solomonic”决定,我只是敢说, 依赖性倒置原则 (DIP)是最被低估的。
While some central concepts in the realm of object-oriented design are generally harder to digest at first, such as separation of concerns and implementation switching, more intuitive and untangled paradigms on the other hand are simpler, like programming to interfaces. Unfortunately, the DIP’s formal definition is surrounded by a double-edged curse/blessing that often makes programmers gloss over it, as in many cases there’s an implicit assumption that the principle is nothing but a fancy expression for the aforementioned “programming to interfaces” commandment:
虽然通常一开始通常较难理解面向对象设计领域中的一些核心概念,例如关注点分离和实现切换,但另一方面,更直观,更复杂的范例更简单,例如对接口进行编程。 不幸的是,DIP的正式定义被双刃咒语/祝福所包围,这常常使程序员对它有所掩饰,因为在许多情况下,隐含的假设是该原理不过是上述“编程接口”命令的理想表达。 :
- High-level modules should not depend on low-level modules. Both should depend on abstractions. 高级模块不应依赖于低级模块。 两者都应依赖抽象。
- Abstractions should not depend upon details. Details should depend upon abstractions. 抽象不应依赖细节。 细节应取决于抽象。
At first blush, the above statements seem to be self-explanatory. Considering at this point no one would disagree that systems built upon a strong dependency on concrete implementations are an ominous sign of bad design, switching over a few abstractions makes perfect sense. So this would lead us right back to the start, thinking that the DIP’s main concern is all about programming to interfaces.
乍一看,上述说法似乎是不言而喻的。 考虑到这一点,没有人会不同意建立在对具体实现的高度依赖之上的系统是不良设计的不祥之兆,切换一些抽象是很有意义的。 因此,这将使我们重新回到起点,以为DIP的主要关注点是关于接口的编程。
In reality, decoupling interface from implementation is just a half-baked approach when it comes to fulfilling the principle’s requirements. The missing portion is achieving the real inversion process. Of course, the question that comes up is: inversion of what?
实际上,将接口与实现脱钩只是满足原则要求的一种半生半熟的方法。 缺失的部分正在实现真正的反演过程。 当然,出现的问题是:什么倒置?
In a traditional sense, systems were always designed to work with high-level components, be it classes or procedural routines, depending upon low-level ones (the details). For instance, a logging module could have a strong dependency on a set of concrete loggers down the chain which actually log information onto the system. It’s no surprise then this scheme would noisily ripple side effects up toward the upper layers whenever the protocol owned by the loggers is modified, even if the protocol has been abstracted away.
在传统意义上,系统总是设计为与高级组件一起使用,无论是类还是程序例程,具体取决于低级组件(细节)。 例如,日志记录模块可能强烈依赖于一系列具体的记录器,这些记录器实际上将信息记录到系统中。 不足为奇的是,只要记录器拥有的协议被修改,即使该协议已被抽象化,该方案也会在较大程度上将副作用向上层蔓延。
The implementation of the DIP though helps to mitigate to some extend these ripples by making the logging module own the protocol instead, therefore inverting the overall dependency’s flow. Upon inversion, the loggers should adhere faithfully to the protocol hence changing accordingly and accommodating themselves to its fluctuations if one ever occurs further down the road.
DIP的实现虽然通过使日志记录模块拥有协议来帮助减轻了某些涟漪,但因此却颠倒了整体依赖性的流程。 倒置时,记录员应忠实遵守该协议,因此应进行相应的更改,并在以后发生变化时适应其波动。
In a nutshell, this shows that the DIP is, under the hood, slightly more complex than just relying on the heaps of benefits brought by a standard interface-implementation decoupling. Yes it talks about making both high and low-level modules dependent on abstractions, but at the same time the high-level modules must have ownership of those abstractions – a subtle yet relevant detail that just can’t be overlooked so easily.
简而言之,这表明DIP在幕后比仅依靠标准接口实现去耦所带来的好处要复杂得多。 是的,它谈论的是使高层模块和低层模块都依赖于抽象,但是与此同时,高层模块必须拥有这些抽象的所有权–一个微妙而又相关的细节,不能轻易被忽略。
As you might expect, one way that may help you more easily understand what’s actually under the umbrella of the DIP is through a few hands-on code samples. Therefore, in this article I’ll be setting up some examples so you can learn how to get the most out of this SOLID principle when developing your PHP applications.
如您所料,一种可以帮助您更轻松地了解DIP实质的方法是通过一些动手代码示例。 因此,在本文中,我将建立一些示例,以便您可以了解如何在开发PHP应用程序时充分利用SOLID原理。
开发一个朴素的存储模块(DIP中缺少的“ I”) (Developing a Naive Storage Module (the missing “I” in the DIP))
There are many developers, particularly those with an aversion to the chilly waters of object-oriented PHP, who tend to see the DIP and other SOLID principles as rigid dogma that pushes hard against the language’s inherent pragmatism. I can understand such thinking, as it’s rather difficult to find pragmatic PHP examples in the wild that demonstrate the principle’s real benefits. Not that I want to play myself up as an enlightened programmer (that suit just doesn’t fit well), but it’d be useful to strive for a good cause and demonstrate from a practical standpoint how to implement the DIP in a realistic use case.
有许多开发人员,尤其是那些讨厌面向对象PHP的开发人员,他们倾向于将DIP和其他SOLID原理视为僵化的教条,这与语言固有的实用主义背道而驰。 我能理解这种想法,因为在野外很难找到能证明该原理真正好处的实用PHP示例。 并不是说我想以一个开明的程序员(适合自己的能力不强)发挥自己的作用,而是努力争取一个好的事业并从实践的角度演示如何在实际使用中实现DIP很有用。案件。
To start things off, consider the implementation of a simple file storage module. The module is charged with the task of reading from and writing data to a specified target file. At a very minimalist level, the module in question could be written like this:
首先,考虑一个简单的文件存储模块的实现。 该模块负责读取和写入指定目标文件的数据。 在最低限度的级别上,所讨论的模块可以这样编写:
<?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());
}
}
}
The module is a pretty naive structure, composed of just a few basic components. The first class reads and writes data to the file system, and the second is a simplistic PHP serializer used internally to generate a storable representation of the data.
该模块是一个非常幼稚的结构,仅由几个基本组件组成。 第一类读取数据并将其写入文件系统,第二类是内部使用的简化PHP序列化程序,用于生成数据的可存储表示形式。
These sample components neatly do their business in isolation, and could be wired together to work in sync as follows:
这些示例组件可以独立地巧妙地开展业务,可以将它们连接在一起以同步工作,如下所示:
<?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();
At first glance, the module exhibits fairly decent behavior considering that its functionality allows to save and fetch a nice variety of data from the file system without much hassle. Moreover, the FileStorage
class injects in the constructor a Serializable
interface, thus depending on the flexibility provided by an abstraction rather than a rigid concrete implementation. With this slew of benefits shining bright on their own, what could be wrong with the module?
乍一看,考虑到其功能允许从文件系统中保存和获取各种数据而没有太多麻烦,该模块表现出相当不错的性能。 而且, FileStorage
类在构造函数中注入一个Serializable
接口,因此取决于抽象提供的灵活性,而不是严格的具体实现。 凭借众多优势可以自己发光,该模块可能有什么问题?
As usual, shallow first impressions can be tricky and blurred. Looking a bit closer, not only is it clear FileStorage
actually depends on the serializer, but because of this tight dependency, storing and pulling in data from the target file is constrained to using only PHP’s native serializing mechanism. What would happen if the data must be passed along to an external service as XML or JSON? The carefully-crafted module isn’t reusable anymore. Sad but true!
像往常一样,浅的第一印象可能很棘手且模糊。 再仔细一点,不仅可以看出FileStorage
实际上依赖于序列化程序,而且由于这种紧密的依赖关系,从目标文件中存储和提取数据仅限于使用PHP的本机序列化机制。 如果必须将数据作为XML或JSON传递给外部服务,将会发生什么情况? 精心设计的模块不再可重复使用。 悲伤但真实!
The situation raises a few interesting points. First and foremost, FileStorage
still exhibits a strong dependency on the low-level Serializer
, even when the protocol that makes them interoperate with each other has been isolated from the implementation. Second, the level of genericity exposed by the protocol in question is very restrictive, limited to merely swapping out one serializer for another. Not only is depending on an abstraction a delusive perception in this case, but the real inversion process encouraged by the DIP is never achieved.
情况提出了一些有趣的观点。 首先,即使将实现彼此互操作的协议与实现隔离开来, FileStorage
仍然对低级Serializer
表现出强烈的依赖性。 其次,所讨论协议公开的通用性级别非常严格,仅限于将一个串行器换成另一个。 在这种情况下,不仅依赖于抽象会产生幻觉,而且DIP鼓励的真正反演过程也从未实现。
It’s possible to refactor some portions of the file module to make it adhere faithfully to the DIP’s mandates. In doing so, the FileStorage
class would acquire the ownership of the protocol utilized to store and pull in file data, thus getting rid of the dependency on a lower-level serializer and affording you the ability to switch between several storage strategies at runtime. In doing so, you’d be actually gaining a lot of flexibility for free. Therefore, let’s continue along and see how to turn the file storage module into a true DIP-compliant structure.
可以重构文件模块的某些部分,以使其忠实地遵守DIP的要求。 这样, FileStorage
类将获取用于存储和提取文件数据的协议的所有权,从而摆脱对低级序列化程序的依赖,并使您能够在运行时在几种存储策略之间进行切换。 这样,您实际上将免费获得许多灵活性。 因此,让我们继续前进,看看如何将文件存储模块变成真正的DIP兼容结构。
从实现中反转协议所有权和去耦接口(充分利用DIP) (Inverting Protocol Ownership and Decoupling Interface from Implementation (getting the most out of the DIP))
While there aren’t a plethora of options per se, nevertheless there are a few approaches that can be employed for effectively inverting the ownership of the protocol between the FileStorage
class and its low-level collaborator while still keeping the protocol abstract. There’s one in particular, though, which turns out to be pretty intuitive because it relies on the natural encapsulation provided right out of the box by PHP’s namespaces.
尽管本身没有太多选择,但是可以使用一些方法来有效地反转FileStorage
类与其低级协作者之间的协议所有权,同时仍然保持协议抽象。 但是,特别要注意的是,它非常直观,因为它依赖于PHP名称空间直接提供的自然封装。
To translate this somewhat intangible concept into concrete code, the first change that should be made to the module is to define a more relaxed protocol for saving and retrieving file data, that way it is easy to manipulate it through formats other than just PHP serialization.
为了将此无形的概念转换为具体的代码,应对模块进行的第一个更改是定义一个更宽松的协议,用于保存和检索文件数据,这样就可以通过除PHP序列化之外的其他格式轻松地对其进行操作。
A slim, segregated interface like the one shown below does the trick with elegance and simplicity:
苗条,隔离的界面(如下图所示)巧妙而优雅地完成了这项工作:
<?php
namespace LibraryFile;
interface EncoderInterface
{
public function encode($data);
public function decode($data);
}
The existence of the EncoderInterface
doesn’t seem to have a profound impact in the file module’s overall design, but it does a lot more than what it promises at face value. The first improvement is the definition of a highly-generic protocol for encoding and decoding data. The second, which is equally important as the first one, is that now the protocol’s ownership belongs to the FileStorage
class since the interface lives and breaths in the class’ namespace. Simply put, we’ve managed to make still undefined low-level encoders/decoders depend on the high-level FileStorage
just by writing a properly namespaced interface. In a nutshell, this is the actual inversion process that the DIP promotes behind its academic veil.
EncoderInterface
的存在似乎并没有对文件模块的整体设计产生深远的影响,但是它的作用远不止于其表面价值。 第一个改进是定义了用于编码和解码数据的通用协议。 第二个与第一个同等重要,是因为接口在该类的命名空间中存在和存在,因此该协议的所有权现在属于FileStorage
类。 简而言之,我们仅通过编写适当的命名空间接口就设法使仍未定义的低层编码器/解码器依赖于高层FileStorage
。 简而言之,这是DIP在其学术面纱背后推动的实际反演过程。
Naturally, the inversion would be a clumsy halfway attempt if the FileStorage
class wasn’t amended to inject an implementer of the previous interface, so here’s the refactored version:
自然地,如果不对FileStorage
类进行修改以注入前一个接口的实现者,那么转换将是一项笨拙的中途尝试,因此,这是重构的版本:
<?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());
}
}
}
With FileStorage
now declaring explicitly in the constructor the ownership of the encoding/decoding protocol, the only thing left to do is to create a set of concrete low-level encoders/decoders, thus allowing you handle file data in multiple formats.
现在FileStorage
在构造函数中明确声明了编码/解码协议的所有权,剩下要做的就是创建一组具体的低层编码器/解码器,从而允许您以多种格式处理文件数据。
The first of these components is just a refactored implementation of the PHP serializer written before:
这些组件中的第一个只是之前编写PHP序列化程序的重构实现:
<?php
namespace LibraryEncoderStrategy;
class Serializer implements LibraryFileEncoderInterface
{
protected $unserializeCallback;
public function __construct($unserializeCallback = false) {
$this->unserializeCallback = (boolean) $unserializeCallback;
}
public function getUnserializeCallback() {
return $this->unserializeCallback;
}
public function encode($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 decode($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;
}
}
It would certainly be redundant to dissect the logic behind Serializer
. Even though, it’s worth to pointing out that it not only depends now upon a more permissive encoding/decoding abstraction, but the abstraction’s ownership is explicitly exposed at the namespace level.
剖析Serializer
背后的逻辑当然是多余的。 即使,值得指出的是,它现在不仅依赖于更宽松的编码/解码抽象,而且抽象的所有权明确地在名称空间级别公开。
Likewise, we could go one step further and set out to write a few more encoders so that the benefits brought by the DIP are highlighted. With that said, here’s how another additional low-level component could be written:
同样,我们可以更进一步,着手编写更多的编码器,从而突出显示DIP带来的好处。 话虽如此,这是如何编写另一个附加的低级组件的方法:
<?php
namespace LibraryEncoderStrategy;
class JsonEncoder implements LibraryFileEncoderInterface
{
public function encode($data) {
if (is_resource($data)) {
throw new InvalidArgumentException(
"PHP resources cannot be JSON-encoded.");
}
if (($data = json_encode($data)) === false) {
throw new RuntimeException(
"Unable to JSON-encode the supplied data.");
}
return $data;
}
public function decode($data) {
if (!is_string($data) || empty($data)) {
throw new InvalidArgumentException(
"The data to be decoded must be a non-empty string.");
}
if (($data = json_decode($data)) === false) {
throw new RuntimeException(
"Unable to JSON-decode the supplied data.");
}
return $data;
}
}
As expected, the underlying logic that’s behind extra encoders generally resembles that of the first PHP serializer, except for any obvious refinements and variants. Additionally, the components conform to the requirements imposed by the DIP, therefore adhering to the encoding/decoding protocol defined within the FileStorage
namespace.
正如预期的那样,除了任何明显的改进和变体之外,额外编码器背后的底层逻辑通常类似于第一个PHP序列化程序的逻辑。 此外,这些组件符合DIP提出的要求,因此遵守FileStorage
名称空间中定义的编码/解码协议。
With both high-level and low-level components in the file module depending upon an abstraction, and encoders exposing a clear dependency on the file storage class, we could safely claim that the module behaves as a true DIP-compliant structure.
由于文件模块中的高层组件和低层组件都取决于抽象,并且编码器暴露了对文件存储类的明确依赖性,因此我们可以放心地声明该模块的行为就像是一个真正的DIP兼容结构。
Moreover, the following example shows how to bring the components together:
此外,以下示例显示了如何将组件组合在一起:
<?php
use LibraryLoaderAutoloader,
LibraryEncoderStrategyJsonEncoder,
LibraryEncoderStrategySerializer,
LibraryFileFileStorage;
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();
$fileStorage = new FileStorage(new JsonEncoder);
$fileStorage->write(new stdClass());
print_r($fileStorage->read());
$fileStorage = new FileStorage(new Serializer);
$fileStorage->write(array("This", "is", "a", "sample", "array"));
print_r($fileStorage->read());
Aside from some naive subtleties that the module exposes to client code, it’s useful for making the point and to demonstrate in a pretty instructive manner why the DIP’s predicates are actually more extensive than the old “programming to an interface” paradigm. It describes and explicitly prescribes an inversion of dependencies, and as such, it should be fulfilled through different mechanisms. PHP’s namespaces are a great way to accomplish this without excessive burden, even though traditional approaches like defining well-structured, expressive application layouts can yield the same results.
除了模块暴露给客户端代码的一些天真细微之处外,它对于阐明这一点并以非常有启发性的方式展示为什么DIP的谓词实际上比旧的“编程为接口”范式更为有用。 它描述并明确规定了依赖关系的反转,因此,应通过不同的机制来实现。 PHP的命名空间是一种无需过多负担即可完成此任务的好方法,即使传统方法(如定义结构良好,表现力强的应用程序布局)可以产生相同的结果。
结束语 (Closing Remarks)
In general, opinions based on subjective expertise tend to be rather biased, and certainly the ones I expressed at the beginning of this article aren’t any exceptions. There is, however, a slight tendency to overlook the Dependency Inversion Principle in favor of its more convoluted SOLID counterparts, as it’s quite easy to misunderstand it as a synonym for dependency on abstractions. Furthermore, some programmers tend to react intuitively and think of the term “Inversion” as an abbreviated expression for Inversion of Control, which, although related to each other, is ultimately a wrong conception.
总的来说,基于主观专业知识的观点往往带有偏见,当然,我在本文开头所表达的观点也不例外。 但是,有一种略微的趋势会忽略依赖倒置原则,而采用复杂的SOLID对应项,因为很容易将其误解为依赖抽象的同义词。 此外,一些程序员往往直观地React并认为术语“反演”的作为缩略表达控制反转 ,其虽然彼此相关,最终是一个错误的概念。
Now that you know what’s really under the hood of the DIP, make sure to exploit all of benefits that it brings to the table, something that will surely make your applications a lot less vulnerable to Fragility and Rigidity issues that might eventually arise as they grow over time.
既然您已经了解了DIP的真正含义,请确保充分利用DIP带来的所有好处,这肯定会使您的应用程序不那么容易受到脆弱性和刚性问题的影响,而脆弱性和刚性问题可能随着它们的增长而逐渐出现随着时间的推移。
Image via kentoh/ Shutterstock
图片来自kentoh / Shutterstock
依赖倒置原则的核心