Dependency Inversion Principle (DIP), What & How ?

Dependency Inversion Principle (DIP), What & How ?

You have probably already seen this picture

CleanArchitecture-Uncle-Bob.jpg

Today it is not enough to just create a working application, the world is changing, and the system must be able to adapt to the change. Code that is not readable, flexible and maintainable is no different from a bomb. The SOLID concept is one of the approaches that must be adopted if you want to write clean code.

SOLID is an acronym that covers several concepts :

S : Single Responsibility Principle (SRP), A class should have one and only one reason to change, meaning that a class should have only one job.

O : Open/Closed Principle (OCP), Objects or entities should be open for extension but closed for modification.

L : Liskov Substitution Principle (LSP), If B and C are implementations of A, then B and C must be interchanged without affecting the execution of the program.

I : Interface Segregation Principle (ISP), The caller should not know the methods he does not have to use.

D : Dependency Inversion Principle (DIP), Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.

It would be unfair to tell you that any one of the SOLID principles is more important than another. In this article we are going to talk about dependency inversion (DIP)

Dependency inversion, D from SOLID.

This Principle was defined by Robert C. Martin in his book Agile Software Development, Principles, Patterns, and Practices.

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.

The main purpose of dependency inversion is not to let a class depend on other classes. It should only depend on their abstractions.

As for the graph above, a higher level layer must use an abstraction to communicate with the lower level layer in order to reduce the tight coupling of code.

E.g. : If changing your database management system forces you to change your business logic, there is a big problem !

let's code

the-matrix-reloaded-matrix.gif

We have an application that is supposed to play music, initially we want to play audio songs.

We have a class AudioSong and a Player that will play the song.

class AudioSong {
    fun play() {
        print("Playing audio")
    }
}
class Player(val audioSong: AudioSong) {
    fun launch() {
        this.audioSong.play()
    }
}

Then to launch the song, I would have only to pass the song in the constructor and launch the player

val audioSong = AudioSong()
val player = Player(audioSong)

At first everything works fine, but our system needs to evolve, and we want to add other types of songs, e.g. : videos.

class VideoSong {
    fun play() {
        print("Playing video song")
    }
}

We have a problem, our player only supports audio songs. How to make it support more than one type of songs? If we create a player for each type of song, it means that for 100 types we will have 100 players. Not easy to manage and maintain, and it's an infraction to the DRY (Don't Repeat Yourself) pattern.

How to proceed?

Solution.gif

For this we need to create an abstraction. Our player will know that it plays a song, but not which one, this will allow us to change and even add other types without breaking the system.

The interface is the best abstraction

we will just have to create an interface that will represent a song with all its methods

interface Song {
    fun play()
}

our songs will only have to implement the interface and overload its methods

class AudioSong : Song {
    override fun play() {
        print("Playing audio song")
    }
}
class VideoSong : Song {
    override fun play() {
        print("Playing video song")
    }
}

and use the interface in our player

class Player(val song: Song) {

    fun launch() {
        this.song.play()
    }

}

our player now supports all types of songs

val audioSong = AudioSong()
val player1 = Player(audioSong)

val videoSong = VideoSong()
val player2 = Player(videoSong)

player1.launch() // "Playing audio song"
player2.launch() //"Playing video song"

We can add other types, they will only have to implement the Song interface

Conclusion !

An application is created to solve problems, if it is badly designed it will be a problem itself.

Dependency inversion is a rather simple pattern to implement. And in simple cases, you can apply it without a framework and enjoy its advantages at a lower cost: decoupling, modularity, testability.

I hope this article has helped you, thanks for reading it and see you soon!