Quoting Uncle Bob – “DIP- The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.”
I’ll try to break this down: In Java, abstractions translate to interfaces or abstract classes where as concretions are implementations of these interfaces or abstract classes. Let’s rewrite this definition with this in mind: DIP tells us that the most flexible systems are those in which source code dependencies refer only to interfaces/abstract classes, not to implementations of these interfaces/abstractions.
It makes sense I think, but why do we have to do it? As a wise man(me!!) once said: Change in Software Development(or in life) is the only constant! Think of this: Long time ago, offer letters were to be handwritten, then typewriters were invented and letters were typed. Later computers were invented and keyboard did the job. Currently with many companies going paperless, we see E-Offer Letters to be digitally signed. If we were to implement such an example it may look something like this:
package com.parking.lot.com.parking.hr;
public class HiringProcess {
public void hireACandidate() {
getCandidateInformation();
boolean isSelected = conductInterview();
if (isSelected) {
releaseOffer();
}
}
private void getCandidateInformation() {
}
private boolean conductInterview() {
return false;
}
public void releaseOffer() {
// HandWritten Letters to be sent via Post
// Typewriter used to type letters and sent via Post
// Computers used to type letters, print the copy of letters and sent via Post
// Computers used to type letters copy sent to email directly
}
}
Not the best code we see but enough to explain the concept. We could perhaps segregate and assign the responsibility of releasing offer letters to ReleaseHandWrittenOfferLetter, ReleaseTypeWriterBasedOfferLetter or ReleaseComputerBasedOfferLetter and ReleaseEOfferLetter.
package com.parking.lot.com.parking.hr;
public class HiringProcess {
ReleaseHandWrittenOfferLetter releaseOfferLetter = new ReleaseHandWrittenOfferLetter();
public void hireACandidate() {
getCandidateInformation();
boolean isSelected = conductInterview();
if (isSelected) {
releaseOfferLetter.releaseOffer();
}
}
private void getCandidateInformation() {
}
private boolean conductInterview() {
return false;
}
}
class ReleaseHandWrittenOfferLetter {
public void releaseOffer() {
// HandWritten Letters to be sent while Post
}
}
class ReleaseTypeWriterBasedOfferLetter {
public void releaseOffer() {
// Typewriter used to type letters and sent via Post
}
}
class ReleaseComputerBasedOfferLetter {
public void releaseOffer() {
// Computers used to type letters, print the copy of letters and sent via Post
}
}
class ReleaseEOfferLetter {
public void releaseOffer() {
// Computers used to type letters copy sent to email directly
}
}
Over the years, we would have changed ReleaseHandWrittenOfferLetter, ReleaseTypeWriterBasedOfferLetter, etc. many times, sometimes even deleted redundant code. If we were to have all these functions to co-exist in the system then we can only imagine the painful if-else statements floating all over, staring at you, rekindling the questions that we usually get while taking a shower!
One problem with the code could be that ReleaseOfferLetterSomethingStupidThatFollows way of handling things is that the implementation is concrete not abstraction. How would an abstraction help here? Think about this: if we were to have ReleaseOfferLetter as an interface and multiple implementations indicating of changing times over the years and the way we rollout offer letters, we can have all the implementations to co-exist and the main Hiring class remains absolutely indifferent of the changes being in the offer rollout process!
package com.parking.lot.com.parking.hr;
public class HiringProcess {
ReleaseOfferLetter releaseOfferLetter; // get this using factory pattern
public void hireACandidate() {
getCandidateInformation();
boolean isSelected = conductInterview();
if (isSelected) {
releaseOfferLetter.releaseOffer();
}
}
private void getCandidateInformation() {
}
private boolean conductInterview() {
return false;
}
}
interface ReleaseOfferLetter {
public void releaseOffer();
}
class ReleaseHandWrittenOfferLetter implements ReleaseOfferLetter {
@Override
public void releaseOffer() {
// HandWritten Letters to be sent while Post
}
}
class ReleaseTypeWriterBasedOfferLetter implements ReleaseOfferLetter {
@Override
public void releaseOffer() {
// Typewriter used to type letters and sent via Post
}
}
class ReleaseComputerBasedOfferLetter implements ReleaseOfferLetter {
@Override
public void releaseOffer() {
// Computers used to type letters, print the copy of letters and sent via Post
}
}
class ReleaseEOfferLetter implements ReleaseOfferLetter {
@Override
public void releaseOffer() {
// Computers used to type letters copy sent to email directly
}
}
Looks better doesn’t it? Code is still bad and there is scope of improvement. The way of transmission of offer letter is not the responsibility of ReleaseOfferLetter implementation. We should use SRP to segregate that functionality too but that’s a different discussion altogether.
Another example could be of a remote controller: Imagine you have a remote control and it can turn on and off different appliances in your house. Now, if the remote control is directly connected to each appliance, it will be difficult to change or add new appliances in the future. But If the remote control only talks to a central system that controls all appliances in the house, it will be easy to add new appliances or change old ones without affecting the remote control.
In this example, the central system is like an abstraction, and the remote control only talks to the central system, instead of talking directly to the appliances. The central system controls the appliances and the remote control controls the central system. This way, the remote control is not dependent on any specific appliance, it’s dependent on an abstraction, making it more flexible and easy to maintain.
Another example could be of building a software system for a restaurant:
Imagine you are building a software system for a restaurant. The system should be able to take orders from customers, calculate the total cost of the order, and print a receipt.
One way to implement this system would be to have the order class directly call a method on the printer class to print the receipt, and the printer class directly call a method on the menu class to lookup the prices of items. But this approach would make the order class dependent on both the printer and menu classes, which would make it difficult to change or reuse the classes in the future.
A better approach would be to use the Dependency Inversion Principle. Instead of the order class depending on the printer and menu classes, both the order class and the printer class would depend on an abstraction. For example, the order class could depend on an interface called “IOrderProcessor” and the printer class could depend on an interface called “IPrinter”. These interfaces would define the methods that the order and printer classes expect to be available, such as the “CalculateTotal” and “PrintReceipt” methods.
This example illustrates how Dependency Inversion Principle helps to make a system more flexible, reusable, and testable by reducing dependencies between components and promoting loose coupling.
By following this approach, we could easily change or replace the menu or printer class without affecting the order class. For example, if we wanted to change the printer to print receipts to a web-based system, we would only need to create a new class that implements the “IPrinter” interface, and swap it out with the old printer class. The order class would continue to work because it’s dependent on the interface and not the implementation.
package com.parking.lot.com.parking.hr;
import java.util.List;
public class Test {
}
interface OrderProcessor {
double calculateTotal(List<String> items);
}
interface Printer {
void printReceipt(double total);
}
class Order implements OrderProcessor {
private final IMenu menu;
public Order(IMenu menu) {
this.menu = menu;
}
public double calculateTotal(List<String> items) {
double total = 0;
for (String item : items) {
total += menu.getPrice(item);
}
return total;
}
}
class Menu implements IMenu {
@Override
public double getPrice(String item) {
// Lookup the price of the item from a database or hard-coded values
}
}
interface IMenu {
double getPrice(String item);
}
Here, Order class is dependent on the abstraction(OrderProcessor) and not on the implementation(Menu). This way we can easily change the implementation of the menu, without affecting the order class.
One more example I can think of is of weather forecasting: Imagine you are building a weather forecasting application. The application should be able to display the current temperature, humidity and pressure for a given location.
One way to implement this application would be to have the main weather class directly access the temperature, humidity, and pressure sensor classes to get the current readings. But this approach would make the weather class dependent on the sensor classes, which would make it difficult to change or reuse the classes in the future.
A better approach would be to use the Dependency Inversion Principle. Instead of the weather class depending on the sensor classes, both the weather class and the sensor classes would depend on an abstraction. For example, the weather class could depend on an interface called “WeatherData” and the sensor classes could depend on an interface called “Sensor”. These interfaces would define the methods that the weather class and sensor classes expect to be available, such as the “getTemperature()”, “getHumidity()” and “getPressure()” methods.
By following this approach, we could easily change or replace the sensor classes without affecting the weather class. For example, if we wanted to change the sensor to use a different technology, we would only need to create a new class that implements the “Sensor” interface, and swap it out with the old sensor class. The weather class would continue to work because it’s dependent on the interface and not the implementation.
interface WeatherData {
double getTemperature();
double getHumidity();
double getPressure();
}
interface Sensor {
void updateData();
}
class WeatherDataImpl implements WeatherData {
private Sensor sensor;
private double temperature;
private double humidity;
private double pressure;
public WeatherDataImpl(Sensor sensor) {
this.sensor = sensor;
}
public void updateData() {
sensor.updateData();
}
public double getTemperature() {
return temperature;
}
public double getHumidity() {
return humidity;
}
public double getPressure() {
return pressure;
}
public void setSensor(Sensor sensor) {
this.sensor = sensor;
}
public void setTemperature(double temperature) {
this.temperature = temperature;
}
public void setHumidity(double humidity) {
this.humidity = humidity;
}
public void setPressure(double pressure) {
this.pressure = pressure;
}
}
class TemperatureSensor implements Sensor {
private WeatherDataImpl weatherData;
public TemperatureSensor(WeatherDataImpl weatherData) {
this.weatherData = weatherData;
}
public void updateData() {
// get the current temperature from the sensor
double temperature = 112.2/* sensor value */;
weatherData.setTemperature(temperature);
}
}
class HumiditySensor implements Sensor {
private WeatherDataImpl weatherData;
public HumiditySensor(WeatherDataImpl weatherData) {
this.weatherData = weatherData;
}
@Override
public void updateData() {
}
}
To summarize:
The Dependency Inversion Principle (DIP) has several advantages, including:
- Loose Coupling: DIP promotes loose coupling between modules, making it easier to change or replace one module without affecting the other modules in the system. This makes the system more flexible and easier to maintain.
- Reusability: By depending on abstractions rather than concrete implementations, classes can be reused in different contexts. This makes it easier to reuse existing code and reduces the amount of new code that needs to be written.
- Testability: DIP makes it easier to test classes in isolation, since they are not tightly coupled to other classes. This makes it easier to write automated tests, which can help to improve the overall quality of the code.
- Separation of concerns: DIP promotes the separation of concerns by separating the high-level policy from the low-level detail. This makes it easier to understand the overall structure of the system and to make changes to the system without affecting other parts of the system.
- Open-Closed Principle: DIP is closely related to the open-closed principle, which states that classes should be open for extension but closed for modification. By depending on abstractions, classes can be extended to support new behavior without modifying the existing code.
In summary, DIP is a powerful principle that can help to improve the design of software systems by promoting loose coupling, reusability, testability, separation of concerns, and adherence to the open-closed principle.