Dependency Inversion Principle is fifth and last principle from SOLID acronym. The definition states two things. First: high-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces). Second: abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
In real-life this principle can be compare to devices that can be connected to other devices. For example our computers, they are our high-level modules, if we want to connect to them keybord, mouse or external memory disc which are our low-level modules, we can either connect them permanently, like solder them to motherboard or we can use USB plug and easily remove one device and plug in another device. This principle provides great flexibility to our programs, high-level modules should know only what needs to be done, but they should not know who should do it.
Let’s take our example from Single Responsibility Principle. We have there one class WeightCalculator which is using two other classes WeightCalculationMaker and ResultPrinter. WeightCalculator is a high-level module because to make it work it needs two low-level modules. WeightCalculator depends on WeightCalculationMaker and ResultPrinter. Let’s see this on UML diagram.

This situation is not good because WeightCalculator is tightly coupled to WeightCalculationMaker and ResultPrinter which means that every change in code in low-level modules needs to be reflected in code in high-level module. Let’s also take a look on code.
public class WeightCalculator {
private final WeightCalculationMaker weightCalculationMaker = new WeightCalculationMaker();
private final ResultPrinter resultPrinter = new ResultPrinter();
public void calculateWeight(float weightOnEarth, CelestialBody celestialBody) {
resultPrinter.printResult(weightCalculationMaker.makeWeightCalculation(weightOnEarth, celestialBody));
}
}
public class WeightCalculationMaker {
public String makeWeightCalculation(float weightOnEarth, CelestialBody celestialBody) {
switch (celestialBody) {
case MOON:
return String.valueOf(weightOnEarth * 0.166) + "kg";
case MERCURY:
return String.valueOf(weightOnEarth * 0.378) + "kg";
case VENUS:
return String.valueOf(weightOnEarth * 0.907) + "kg";
case MARS:
return String.valueOf(weightOnEarth * 0.377) + "kg";
case JUPITER:
return String.valueOf(weightOnEarth * 2.528) + "kg";
case SATURN:
return String.valueOf(weightOnEarth * 1.064) + "kg";
case URANUS:
return String.valueOf(weightOnEarth * 0.889) + "kg";
case NEPTUNE:
return String.valueOf(weightOnEarth * 1.125) + "kg";
case PLUTO:
return String.valueOf(weightOnEarth * 0.067) + "kg";
default:
return "";
}
}
}
public class ResultPrinter {
public void printResult(String result) {
System.out.println(result);
}
}
We are going to change this with adding a middle layer to loosely couple high-level module with low-level modules so they will be easy to replace. Let’s add two new interfaces. First one is IWeightCalculationMaker with one method with two parameters.
public interface IWeightCalculationMaker {
String makeWeightCalculation(float weightOnEarth, CelestialBody celestialBody);
}
And the second one is IResultPrinter with one method with one parameter.
public interface IResultPrinter {
void printResult(String result);
}
Then we have our two classes which each implements proper interface.
public class WeightCalculationMaker implements IWeightCalculationMaker {
public String makeWeightCalculation(float weightOnEarth, CelestialBody celestialBody) {
switch (celestialBody) {
case MOON:
return String.valueOf(weightOnEarth * 0.166) + "kg";
case MERCURY:
return String.valueOf(weightOnEarth * 0.378) + "kg";
case VENUS:
return String.valueOf(weightOnEarth * 0.907) + "kg";
case MARS:
return String.valueOf(weightOnEarth * 0.377) + "kg";
case JUPITER:
return String.valueOf(weightOnEarth * 2.528) + "kg";
case SATURN:
return String.valueOf(weightOnEarth * 1.064) + "kg";
case URANUS:
return String.valueOf(weightOnEarth * 0.889) + "kg";
case NEPTUNE:
return String.valueOf(weightOnEarth * 1.125) + "kg";
case PLUTO:
return String.valueOf(weightOnEarth * 0.067) + "kg";
default:
return "";
}
}
}
public class ResultPrinter implements IResultPrinter {
public void printResult(String result) {
System.out.println(result);
}
}
And finally our high-level class which now is using interfaces instead of classes.
public class WeightCalculator {
private IWeightCalculationMaker iWeightCalculationMaker;
private IResultPrinter iResultPrinter;
public WeightCalculator(IWeightCalculationMaker iWeightCalculationMaker, IResultPrinter iResultPrinter) {
this.iWeightCalculationMaker = iWeightCalculationMaker;
this.iResultPrinter = iResultPrinter;
}
public void calculateWeight(float weightOnEarth, CelestialBody celestialBody) {
iResultPrinter.printResult(iWeightCalculationMaker.makeWeightCalculation(weightOnEarth, celestialBody));
}
}
We have added also a constructor with two parameters, now we can easily replace functionality with passing different objects which implement our interfaces. This is called dependency injection. Let’s see main program which is using our updated code.
public class MyProgram {
public static void main(String[] args) {
WeightCalculator weightCalculator = new WeightCalculator(new WeightCalculationMaker(), new ResultPrinter());
weightCalculator.calculateWeight(99, CelestialBody.MOON);
}
}
And after running the above we can see result.
16.434kg
Process finished with exit code 0
Let’s not forget about updated UML diagram.

While designing our modules we need to stop when one of our classes is using instances of other classes. It is better to use instances of interfaces instead of instances of classes.