DIP: The Dependency-Inversion Principle
Decoupling high-level modules from low-level details
Note: This publication demonstrates the Dependency-Inversion Principle in TypeScript and PHP.
DIP stands for Dependency Inversion Principle. It is one of the principles of Object-Oriented Design. Robert Cecil Martin¹ (widely known as Uncle Bob) introduces several principles in his famous book:
Agile Software Development, Principles, Patterns, and Practices
He describes five OOD principles in this book. They are collectively known as the SOLID Principles. These principles have received wide attention in the software industry. The Dependency Inversion Principle is the last one of the five principles.
Uncle Bob defines the Dependency Inversion Principle in his book. The definition is as the following:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
Eric Freeman and Elisabeth Robson, in their Head First Design Patterns book, reference this definition in a concise form. They say:
Depend upon abstractions. Do not depend upon concrete classes.
You may guess somewhat what the DIP is about. It says high-level modules or classes should NOT depend on concrete classes (low-level classes). Again, it says high-level and low-level classes both should depend on the abstractions.
Concrete classes are classes that have implementation details.
These abstractions should not depend on implementation details, rather, implementation details should depend on abstractions.
Violating DIP: High-level Modules or Classes
First, we will see what the high-level modules are. The high-level modules are Client classes. What are client classes? In our case, the clients classes are the classes that have dependencies.
Then, what are low-level modules? The dependent classes are low-level modules or classes.
Do not mess up with Dependency Injection (DI). DIP is completely different from DI. DIP may have a similarity, in terms of appearance, with DI but they are totally different from each other in terms of purpose.
Let us see a code example to make it clear.
// TypeScritpt
class SocialPoster {
private twitter: Twitter;
// Notice the "Twitter" dependent class here
constructor(twitter: Twitter) {
this.twitter = twitter;
}
post(data: PostData): void {
this.twitter.post(data);
}
}
// PHP
class SocialPoster
{
private Twitter $twitter;
// Notice the "Twitter" dependent class here
public function __construct(Twitter $twitter)
{
$this->twitter = $twitter;
}
public function post(array $data): void
{
$this->twitter->post($data);
}
}
In the code above, the SocialPoster
is the high-level class and the Twitter
class is the low-level class. The DIP says high-level classes should not depend on low-level classes. But we have done so in the code above. What is the problem with it? Can you find anything in the code that seems troublesome to you? Check out below:
- As the
Twitter
class is a concrete class, it is tightly coupled with theSocialPoster
class. It violates the loose coupling principle. - Any changes to the
Twitter
class will make theSocialPoster
class change. It breaks the encapsulation pillar of the OOP. - The
SocialPoster
class is not reusable. This hits one of the advantages of the OOP: reusability. - The
SocialPoster
class is limited to posting onTwitter
social media. It could have made a$socialMedia
parameter polymorphic, thus, this breaks the polymorphism pillar of the OOP.
Depending on the low-level Twitter
class, the high-level SocialPoster
class violates the Dependency Inversion Principle (DIP).
So what is the solution? The solution is somewhat tricky. What we have to do is to invert the dependency. How would we invert the dependency?
Applying DIP: Inversion of Dependency
Generally, when we start learning OOP, we use concrete classes as the dependent classes when a class depends on them. But we have seen dealing with a concrete class as a dependent class has produced some problems. We need to find out a way so that the SocialPoster
class becomes loosely coupled and does NOT depend on any concrete class. How? We can do that inverting dependency!
To invert the dependency, we have to make sure our high-level classes use abstractions. What are abstractions? Abstractions are concepts (not details) or contracts or rules or laws. The abstractions tell what-to-do. But, keep in mind, they do NOT tell you how-to-do. That is why they are called abstractions.
Let us find out why the high-level class SocialPoster
needs a dependency on a low-level class the Twitter
class. There could have many reasons for using the Twitter
class. But, in our case, the main reason is to post on that social media.
Let us abstract posting on social media. We can invert the dependency using either an abstract interface or an abstract class. That means we will pass an abstract interface to the constructor of the client class SocialPoster
instead of a concrete class. Thus depending on an abstraction, not a concrete class, the SocialPoster
class inverts managing its dependency tradition. So make a contract/rule/law.
// TypeScript
interface SocialPosterInterface {
/**
* Posts on social media
*
* @param {PostData} data
* @return {void}
*/
post(data: PostData): void;
}
// PHP
interface SocialPosterInterface
{
/**
* Posts on social media
*
* @param array data
* @return void
*/
public function post(array $data): void;
}
This is our abstraction for posting on social media. The abstraction provides such a law that dictates what you have to abide by. In the code above, the abstraction, the contract dictates it has a post()
method that must be used to post on social media (instead of using a method from a concrete class).
Now we can make the SocialPoster
class depend on the abstraction — the SocialPosterInterface
.
// TypeScript
class SocialPoster {
private socialMedia: SocialPosterInterface;
constructor(socialMedia: SocialPosterInterface) {
this.socialMedia = socialMedia;
}
post(data: PostData): void {
this.socialMedia.post(data);
}
}
// PHP
class SocialPoster
{
private SocialPosterInterface $socialMedia;
public function __construct(SocialPosterInterface $socialMedia)
{
$this->socialMedia = $socialMedia;
}
public function post(array $data): void
{
$this->socialMedia->post($data);
}
}
As you can see the SocialPoster
class manages its dependency based on abstraction, so there is no need to depend on the low-level classes. No matter how the post()
method of SocialPosterInterface
is implemented. It just has the knowledge about the contract/rule/law but not about the implementation details. The SocialPoster
class is no more coupled with a limited API. It has now unlimited power. Because, it can receive any kind of social media API object as an argument, even if the one has not been invented yet. So it is tremendously scalable and reusable.
The $socialMedia
parameter of the SocialPoster
constructor method has become polymorphic because of using abstraction. You can use any kind of object as an argument that implements the SocialPosterInterface
.
Low-level Modules/Classes
The SocialPoster
class is now decoupled in the sense that if you change in the low-level class (for example, Twitter
class) does not affect the SocialPoster
class anymore. Moreover, it can be reused in any context where low-level modules conform to the SocialPosterInterface
abstract interface.
Remember abstraction provides one or more concepts that tell you what-to-do but not how-to-do. The how-to-do will be implemented by the low-level classes. So whatever we need to change in the low-level classes will not affect the high-level SocialPoster
class unless the low-level classes break the rule provided by that abstract interface.
As we have abstracted posting on social media we can now create as many low-level classes as we need conforming to SocialPosterInterface
abstract interface.
// TypeScript
class Twitter implements SocialPosterInterface {
post(data: PostData): void {
console.log('Posting on twitter...');
console.log(data.message);
}
}
class Facebook implements SocialPosterInterface {
post(data: PostData): void {
console.log('Posting on facebook...');
console.log(data.message);
}
}
// PHP
class Twitter implements SocialPosterInterface
{
public function post(array $data): void
{
var_dump('Posting on twitter...');
var_dump($data['message']);
}
}
class Facebook implements SocialPosterInterface
{
public function post(array $data): void
{
var_dump('Posting on facebook...');
var_dump($data['message']);
}
}
Now the abstraction is not depending on details, rather, details are depending on the abstraction. The SocialPosterInterface
(the abstraction) does not depend on the Twitter
or the Facebook
class (the concrete classes, therefore, implementation details). And the Twitter
and the Facebook
low-level classes depend on the SocialPosterInterface
which is an abstraction.
You can create more low-level classes. Just abide by the contract. It will fit in the high-level SocialPoster
class. You can now see the power of DIP.
Inversion of Ownership
Have you noticed anything? Something has happened after implementing the DIP. And that is an inversion of ownership. Ownership of what? Ownership of the interface. Who owns the interface?
After applying the DIP, it seems that the SocialPoster
owns the ownership of the SocialPosterInterface
. But we have seen the Twitter
and the Facebook
classes both use the SocialPosterInterface
. The ownership of the SocialPosterInterface
has been ambiguous. Should we put it in the high-level module or the low-level module? Does SocialPoster
own it? Or does the Twitter
class or the Facebook
class own it?
But why is this important? See what Uncle Bob says:
Interface needs to stand alone without belonging to either group.
Because the interface can be used by lots of different clients and implemented by lots of different modules. That is why we need to take care of the inversion of the interface ownership.
The DIP has made it happen the inversion of interface ownership. So this is a problem. We can solve it by providing a very generic name to the interface so that does not imply the use of high-level and low-level modules together.
If we change the name of SocialPosterInterface
to PostInterface
or Poster
or Postable
then it may be kept in a separate module. It may be used later by different modules. The Postable
abstract interface is NOT now limited to being used for social postings. It may be used for blog postings, forum postings, and maybe for others.
// TypeScript
interface Postable {
/**
* Publishes post
*
* @param {PostData} data
* @return {unknown}
*/
post(data: PostData): unknown;
}
// PHP
interface Postable
{
/**
* Publishes post
*
* @param array<string, mixed> data
* @return mixed
*/
public function post(array $data): mixed;
}
So our high-level class SocialPoster
should now look like the below.
// TypeScript
class SocialPoster {
private socialMedia: Postable;
constructor(socialMedia: Postable) {
this.socialMedia = socialMedia;
}
post(data: PostData): void {
this.socialMedia.post(data);
}
}
// PHP
class SocialPoster
{
private Postable $socialMedia;
public function __construct(Postable $socialMedia)
{
$this->socialMedia = $socialMedia;
}
public function post(array $data): void
{
$this->socialMedia->post($data);
}
}
Summary
The DIP is one of the most powerful OO design principles. It produces reusable code. It reduces tight-coupling and code duplication. It helps you write the least code. Applying the DIP makes code testable and scalable. The DIP allows you to write code that can be compatible with the code that will be written in the future.
Learning DIP, you would be able to write robust code and easily deal with more design principles such as Design by Contract (DBC), Do Not Repeat Yourself (DRY), Open-Closed Principle (OCP), Loose Coupling or Decoupling, and maybe more. And you will learn how to write polymorphic code.
See full code here: TypeScript and PHP.
Thanks for reading
- 👏 Please clap for this publication
- 🔔 Follow me: Medium | LinkedIn | Twitter
- 🫵 Read more on writing Efficient, Reusable, and Maintainable code