SOLID and Dependency Inversion principle

Overview

In this series of articles, we will cover all the SOLID patterns and their use in examples.

The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.”

The SOLID principles consist of the following five concepts:

Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle

dip

Dependency Inversion Principle

Depend in the direction of abstraction. High level modules should not depend upon low level details.

Why

In the previous principles: Single Responsibility, Open/Closed, Liskov Substitution and Interface segregation we discussed the multiple relationships between components, their violations, and their solutions.

Now, let’s look at the last but not the least important principle: the Dependency Inversion Principle The definition of the Dependency Inversion Principle states that entities must depend on abstractions, not on concretions. This means that the high-level module should not depend on the low-level module, but should depend on abstractions instead. First, let’s clearly define what we mean by low-level and high-level modules in this context.

Low-level vs High-level

Low-level modules represents implementation details that are required to execute the business policies. They are sometimes referred to as the plumbing or the internals of a system. They dictate how the software should perform various tasks. Here are some examples of low‑level modules: Logging, data access, network communication, input and output file operations, the user interface, or telemetry, etc.

On the other hand, A high‑level module is a module or component created to solve real problems and use cases. They are usually more abstract and map directly to the business domain or business logic.

High‑level modules work together with low‑level modules to make an application work.

How apply this principle

Let’s look at samples that are violations of this principle:

public class WeatherTracker {
    public String currentConditions;
    private Phone phone;
    private Emailer emailer;

    public WeatherTracker() {
        phone = new Phone();
        emailer = new Emailer();
    }

    public void setCurrentConditions(String weatherDescription) {
        this.currentConditions = weatherDescription;
        if (weatherDescription == "rainy") {
            String alert = phone.generateWeatherAlert(weatherDescription);
            System.out.print(alert);
        }
        if (weatherDescription == "sunny") {
            String alert = emailer.generateWeatherAlert(weatherDescription);
            System.out.print(alert);
        }
    }
}

Here, we have the WeatherTracker class, which represents the high-level module responsible for tracking the weather. On the other hand, we have devices that should be notified about the weather, which represent low-level modules.

One possible solution would be to put another layer of abstraction between WeatherTracker and any device that would like to be notified.

We could create an interface called Notifier, which would be implemented by Email and Phone classes.

public interface Notifier {
    void alertWeatherConditions(String weatherConditions);
}

public class Email implements Notifier {
    @Override
    public void alertWeatherConditions(String weatherConditions) {
        if (weatherConditions == "sunny");
            System.out.print("It is sunny");
    }
}

public class Mobile implements Notifier {
    @Override
    public void alertWeatherConditions(String weatherConditions) {
        if (weatherConditions == "rainy")
            System.out.print("It is rainy");
    }
}

And then we can refactor WeatherTracker class to use these refactored devices and abstracted interface.

public class WeatherTracker {
    public String currentConditions;

    public void setCurrentConditions(String weatherDescription) {
        this.currentConditions = weatherDescription;
    }

    public void notify(Notifier notifier) {
        notifier.alertWeatherConditions(currentConditions);
    }
}

And when we decide to add any new device to be notified, we can simply implement the current interface and do not change high-level module (WeatherTracker class in this case).

And what about packages

Let take a look on this diagram.

without

As you can see package A has a Class or Object A which reference to another object in different package. Without Dependency Inversion Principle package B needs to compiled before package A.

On second diagram we use interface inside of package A which is inherited by object B in package B. So Compile time dependency is inverted. And this approach can be used when we have multiple different adapter which can be complicated after our main (package A) object already exists.

without

There are two another design principles which are related to this principle however they are not Dependency Inversion Principle.

not Dependency Inversion Principle!

Dependency Injection and Inversion of Control

Dependency Injection is a technique that allows the creation of dependent object outside of a class and provides those objects to a class.

We can declare dependencies as abstractions in the constructor

public class Store {
    private Item item;
 
    public Store() {
        item = new ItemImpl1();    
    }
}
public class Store {
    private Item item;
    public Store(Item item) {
        this.item = item;
    }
}

Or we can use setter-based dependency injection

@Bean
public Store store() {
    Store store = new Store();
    store.setItem(item1());
    return store;
}

Inversion of Control it is a design principle in which the control of object creation, configuration, and lifecycle is passed to a container or framework. We have multiple benefits: easy to switch between different implementation at runtime, increase program modularity, manages the lifecycle of objects.

  • Do not “new” up objects
  • Control of object creation is inverted
  • Inversion of Control makes sense for services or controllers not for entities
Michal Slovík
Michal Slovík
Java developer and Cloud DevOps

My job interests include devops, java development and docker / kubernetes technologies.