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
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.
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.
Related topic
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