DEV Community

loading...
Cover image for Understanding SOLID Principles: Dependency Inversion

Understanding SOLID Principles: Dependency Inversion

tamerlang profile image Tamerlan Gudabayev ・3 min read

At last, our journey has come to an end.

But now it's time to face the final boss.

DIP aka Dependency Inversion Principle

The final principle of the SOLID bosses.

Gear up, drink your mana potion, and prepare for combat.

Table of Contents

What is the dependency inversion principle?

Let's google that.

We get this:

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.

I get it, it sounds confusing.

But let's break this down:

  1. Client: Your main class/code that runs the high-level module.
  2. High-Level Modules: Interface/Abstraction that your client uses.
  3. Low-Level Modules: Details of your interfaces/abstraction.

What it basically says that imagine you have a car and your different components are:

  1. Client: You as the person driving the car.
  2. High-Level Modules: The steering wheel and the gas/brake peddles.
  3. Low-Level Modules: Engine

Abstractions don't depend on details.

For me, it doesn't matter whether my engine has changed or not, I still should be able to drive my car the same way.

Details should depend upon abstractions.

I would not want an engine that causes the brake to double the speed.

Simple Example

Imagine you have a budget reporting system that reads from a database.

It will look something like this:

<?php 

class BudgetReport {
    public $database;

    public function __construct($database)
    {
        $this->database = $database;
    }

    public function open(){
        $this->database->get();
    }

    public function save(){
        $this->database->insert();
    }
}

class MySQLDatabase {
    // fields

    public function get(){
        // get by id
    }

    public function insert(){
        // inserts into db
    }

    public function update(){
        // update some values in db
    }

    public function delete(){
        // delete some records in db
    }
}

// Client
$database = new MySQLDatabase();
$report = new BudgetReport($database);

$report->open();
Enter fullscreen mode Exit fullscreen mode

Everything works fine, but this code violates the dependency inversion principle because our high-level module BudgetReport concretely depends on the low-level module MySQLDatabase.

This also violates the open-closed principle because what if we wanted a different kind of database such as MongoDB?

We will have to change the BudgetReport class to have if-else statements for it not to break.

To fix this is pretty simple, instead of concretely relying on the database class, we should use an abstraction. We will create a DatabaseInterface which will implement any kind of database we need and finally we can inject our database through the constructor.

interface DatabaseInterface {
    public function get();
    public function insert();
    public function update();
    public function delete();
}

class MySQLDatabase implements DatabaseInterface {
    // fields

    public function get(){
        // get by id
    }

    public function insert(){
        // inserts into db
    }

    public function update(){
        // update some values in db
    }

    public function delete(){
        // delete some records in db
    }
}

class MongoDB implements DatabaseInterface {
    // fields

    public function get(){
        // get by id
    }

    public function insert(){
        // inserts into db
    }

    public function update(){
        // update some values in db
    }

    public function delete(){
        // delete some records in db
    }
}

class BudgetReport {
    public $database;

    public function __construct(DatabaseInterface $database)
    {
        $this->database = $database;
    }

    public function open(){
        $this->database->get();
    }

    public function save(){
        $this->database->insert();
    }
}

// Client
$mysql = new MySQLDatabase();
$report_mysql = new BudgetReport($mysql);

$report_mysql->open();

$mongo = new MongoDB();
$report_mongo = new BudgetReport($mongo);

$report_mongo->open();
Enter fullscreen mode Exit fullscreen mode

Now our BudgetReport does not depend concretely on the database class but on its abstraction DatabaseInterface. This approach also follows the open-closed principle because to add any new database we don't have to change the BudgetReport class. We just need to add a new database class that implements the DatabaseInterface.

Why should I follow this principle?

Because code that violates this principle may become too coupled together, and that makes the code hard to maintain, unreadable, and prone to side effects.

Conclusion

In summary:

  1. The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
  2. Code that doesn't follow this principle can be too coupled, which means you will have a hard time managing the project.

If you have any questions, leave them down in the comments section.

Discussion (1)

pic
Editor guide
Collapse
myleftshoe profile image
myleftshoe

Your example is a much easier way of "grokking" the principle than reading the definition, even it's name. I guess we need a name in order to "put a name" to a concept but sometimes solving a coding problem will lead to a solution where you use a principle without knowing it.

e.g. not sure what db I'll end up using for this project - I'll make it so I can switch databases easily. Which means creating a common interface for the crud methods, abstracting the db specific details away and passing the database in.