Core points
If you were to make an arbitrary “Solomonic” decision about the relevance of each SOLID principle, I would say that the principle of reliance on the inversion (DIP) is the most underestimated. While some of the core concepts in the field of object-oriented design are difficult to understand at first, such as separation of concerns and implementing switching, on the other hand, more intuitive and clearer paradigms are simpler, such as interface-oriented programming. Unfortunately, the formal definition of DIP is shrouded in a double-edged sword-like curse/blessing, which often makes programmers ignore it, because in many cases, people default to the principle as just another statement of the aforementioned “interface-oriented programming” commandment:
At first glance, the above statement seems self-evident. Given that no one currently disagrees that systems built on strong reliance on concrete implementations are a bad omen of bad design, it is completely reasonable to switch some abstractions. So this will bring us back to the starting point, thinking that the main focus of DIP is about interface-oriented programming. In fact, decoupling interfaces from implementations is only a semi-finished method when meeting the requirements of this principle. The missing part is to implement the real inversion process. Of course, the question arises: what is the reversal? Traditionally, systems are always designed to make high-level components (whether they are classes or process routines) depend on low-level components (details). For example, a logging module may have strong dependencies on a series of specific loggers (actually recording information into the system). Therefore, whenever the logger's protocol is modified, this scheme will noisy pass side effects to the upper layer, even if the protocol has been abstracted. However, the implementation of DIP helps to some extent to mitigate these ripples by having the logging module have a protocol, thus inverting the overall dependency flow. After the inversion, the loggers should faithfully abide by the protocol, so if there is a change in the future, they should change accordingly and adapt to the fluctuations of the protocol. In short, this shows that DIP is a little more complicated behind the scenes than relying solely on standard interfaces – implementing decoupling. Yes, it discusses making both high- and low-level modules dependent on abstraction, but at the same time high-level modules must have these abstractions—a subtle but relevant detail that cannot be easily overlooked. As you might expect, one way that might help you understand what DIP actually covers is through some practical code examples. So, in this article, I'll set up some examples so you can learn how to take advantage of this SOLID principle when developing a PHP application.
Develop a simple storage module (the missing "I" in DIP)
Many developers, especially those who hate object-oriented PHP, tend to view DIP and other SOLID principles as rigid dogmas, run counter to the pragmatism inherent in the language. I can understand this idea because it is difficult to find practical PHP examples in the wild that showcase the real benefits of the principle. I'm not trying to tout myself as an enlightened programmer (that suit doesn't fit well), but it's still useful to work hard for a good goal and demonstrate from a practical point of view how to implement DIP in real-life use cases. First, consider the implementation of a simple file storage module. This module is responsible for reading and writing data from a specified target file. On a very simplified level, the modules in the question can 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()); } } }
This module is a fairly simple structure consisting of only a few basic components. The first class reads and writes data from the file system, and the second class is a simple PHP serializer for generating a storable representation of data internally. These sample components perform their business well in isolation and can be connected together like this to work synchronously:
<?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 pretty decent behavior given that the module's functionality allows easy storage and acquisition of various data from the file system. Additionally, the FileStorage class injects a Serializable interface into the constructor, thus relies on the flexibility provided by abstraction rather than a rigid concrete implementation. With these advantages, what are the problems with this module? Often, superficial first impressions can be tricky and vague. If you look closely, not only does FileStorage actually rely on the serializer, but because of this tight dependency, storing and extracting data from the target file is limited to the native serialization mechanism using PHP. What happens if the data must be passed as XML or JSON to an external service? Well-designed modules are no longer reusable. Sad but real! This situation raises some interesting questions. First and foremost, FileStorage still shows a strong dependence on low-level Serializers even if the protocols that make them interoperable are already isolated from the implementation. Second, the level of universality disclosed by the protocol in the problem is very limited, and it is limited to swapping one serializer to another. In this case, relying on abstraction is an illusory perception, and the true inversion process encouraged by DIP is never implemented. Some parts of the file module can be refactored to faithfully comply with DIP requirements. In doing so, the FileStorage class will gain ownership of the protocol used to store and extract file data, thus getting rid of the dependency on low-level serializers and allowing you to switch between multiple storage policies at runtime. Doing so, you will actually get a lot of flexibility for free. So let's move on and see how to convert the file storage module to a truly DIP-compliant structure.
Invert protocol ownership and decoupling interfaces and implementations (maintaining full use of DIP)
While there are not many options, there are still ways to effectively reverse protocol ownership between the FileStorage class and its lower-level collaborators while maintaining the abstraction of the protocol. However, there is a method that is very intuitive because it relies on a natural encapsulation of the PHP namespace out of the box. To translate this somewhat elusive concept into concrete code, the first change that should be made to the module is to define a looser protocol to save and retrieve file data so that it can be easily manipulated in formats other than PHP serialization. A streamlined, isolated interface as shown below can do the job gracefully and simply:
<?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; } }
The existence of EncoderInterface does not seem to have a profound impact on the overall design of the file module, but it does more than those that are superficially promised. The first improvement is the definition of a highly general protocol to encode and decode data. The second improvement is as important as the first one, that is, the ownership of the protocol now belongs to the FileStorage class because the interface exists in the class's namespace. In short, we managed to make the still undefined low-level encoder/decoder depend on the high-level FileStorage just by writing an interface with the correct namespace. In short, this is the actual reversal process that DIP advocates behind its academic veil. Of course, if the FileStorage class is not modified to be an implementer injecting the previous interface, then the inversion will be a clumsy half-way attempt, so here is the refactored version:
<?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 FileStorage now explicitly declares ownership of the encoding/decoding protocol in the constructor, and the only thing left is to create a specific set of low-level encoder/decoders that allow you to handle file data in multiple formats. The first of these components is just a refactoring implementation of the PHP serializer written previously:
<?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();
Analysis The logic behind Serializer is definitely redundant. Nevertheless, it is worth pointing out that it now relies not only on looser encoding/decoding abstractions, but ownership of abstractions is explicitly exposed at the namespace level. Again, we can go a step further and start writing more encoders to highlight the benefits of DIP. Having said that, here is another additional low-level component to be written:
<?php namespace LibraryFile; interface EncoderInterface { public function encode($data); public function decode($data); }
As expected, the underlying logic behind the extra encoder is often similar to the first PHP serializer, except for any noticeable improvements and variants. Additionally, these components comply with DIP-imposed requirements and therefore comply with the encoding/decoding protocols defined in the FileStorage namespace. Since both the upper and lower-level components in the file module rely on abstraction and the encoder has a clear dependency on the file storage class, we can safely claim that the module behaves in line with the DIP specification. Additionally, the following example shows how to combine these components:
<?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()); } } }
Apart from some simple subtlety that the module exposes to client code, it is very useful for explaining the key points and demonstrating in a rather instructive way why DIP's predicates are actually more broadly than the old "interface-oriented programming" paradigm. It describes and explicitly specifies the inversion of dependencies and should therefore be implemented through different mechanisms. PHP's namespace is a great way to achieve this without too much burden, although traditional methods like defining well-structured, highly expressive application layouts can produce the same results.
Conclusion
Often, opinions based on subjective expertise are often biased, and of course, the views I expressed at the beginning of this article are no exception. However, there is a slight tendency to ignore the principle of dependency inversion for its more complex SOLID counterpart, as it is easily misunderstood as a synonym for dependency abstraction. Furthermore, some programmers tend to react intuitively and think of the term "inversion" as an abbreviation expression that controls inversion, and while the two are related to each other, this is ultimately a false concept. Now that you know the true meaning of DIP, be sure to take advantage of all the benefits it brings, which will surely make your application less susceptible to vulnerability and stiffness issues that may arise over time. Pictures from kentoh/Shutterstock
Frequently Asked Questions about the Reliance Inversion Principle
The Dependency Inversion Principle (DIP) is a key aspect of the SOLID principle in object-oriented programming. Its main purpose is to decouple the software module. This means that the high-level module that provides complex logic is separated from the low-level module that provides basic operations. By doing so, changes to low-level modules will have minimal impact on high-level modules, making the entire system easier to manage and maintain.
Traditional programmatic programming usually involves high-level modules relying on low-level modules. This can lead to a rigid system where changes to one module can have a significant impact on the other modules. DIP, on the other hand, inverts this dependency. Both high- and low-level modules rely on abstraction, which promotes flexibility and makes the system more adaptable to changes.
Of course, let's consider a simple program example that reads data from a file and processes it. In traditional methods, processing modules may rely directly on file reading modules. However, with DIP, both modules will rely on an abstraction, such as the "DataReader" interface. This means that the processing module is not bound directly to the file reading module, and we can easily switch to different data sources (such as databases or web services) without changing the processing module.
DIP can bring several benefits to your code. It promotes decoupling, which makes your system more flexible and easier to modify. It also improves the testability of the code, as dependencies can be easily mocked or stubbed. Furthermore, it encourages good design practices such as interface-oriented programming rather than implementation-oriented programming.
While DIP has many advantages, it can also introduce complexity, especially in large systems where the number of abstractions can become difficult to manage. It can also lead to more code writing, as you need to define interfaces and possibly create other classes to implement them. However, these challenges can be mitigated by good design and architectural practice.
DIP is the last principle in the SOLID abbreviation, but it is closely related to other principles. For example, both the Single Responsibility Principle (SRP) and the Open and Close Principle (OCP) promote decoupling, which is a key aspect of DIP. Both the Richter Substitution Principle (LSP) and the Interface Isolation Principle (ISP) deal with abstraction, which is at the heart of DIP.
Absolutely. While DIP is usually discussed in the context of Java and other object-oriented languages, the principle itself is language-independent. You can apply DIPs, such as interfaces or abstract classes in any language that supports abstraction.
A good starting point is to find areas in your code where high-level modules depend directly on low-level modules. Consider whether you can introduce abstractions between these modules to decouple them. Remember that the goal is not to eliminate all direct dependencies, but to ensure that the dependencies are aimed at abstraction, not concrete implementations.
DIP is mainly used to improve the structure and maintainability of the code, rather than its performance. However, by making your code more modular and easier to understand, it can help you identify and resolve performance bottlenecks more efficiently.
While the benefits of DIP are often more obvious in large complex systems, it may also be useful in smaller projects. Even in small code bases, decoupling modules can make your code easier to understand, test, and modify.
The above is the detailed content of The Dependency Inversion Principle. For more information, please follow other related articles on the PHP Chinese website!