Motivation
For the sake of this article, let's imagine a small scenario, where we have a project named "reporting" that is responsible for generating reports for the user. This project uses a library on the side called "printer" that is responsible for printing the reports. The first approach for the solution architecture would be like this:
printer project:
contains the following class to handle the printing jobs:
package printer;
import document.Document;
public class Printer {
public void print(Document document) {
}
}
reporting project:
contains the following class:
package reporting;
import document.Document;
import printer.Printer;
public class ReportGenerator {
private final Printer printer;
public ReportGenerator() {
printer = new Printer();
}
public void generate() {
Document document;
// ...
printer.print(document);
}
}
Note here that we are referencing the Printer and the Document classes, which live in the printer project, from the reporting project. So the compile-time dependency graph looks something like this:
Issue
The problem with the above graph is that any change done in the printer job would require restarting the printer and the reporting job projects. That wouldn't be an issue if "printer" was the main project, but in this case, "reporting" is. Imagine having a bunch of other libraries being used by reporting besides "printer", then any change on any library would require restarting the main project. Technically speaking, this makes our main project coupled and dependent on the side helper projects, and this is the core problem. We would like to have the case reversed, where the libraries depend on the main project.
Concept
What Inversion of Control introduces is the following: let the main project define abstract classes or interfaces of all the external tools it needs at runtime, and whenever an instance of this type is needed, get one from some sort of a global pool. On the other hand, whoever is interested in providing any of the needed services, has to provide the implementation of this interface or abstract class, and register an instance of this implementation in the global pool previously mentioned. This way, the external tools are the ones to depend on the main project at compile time and not the other way around. Also, if all goes well, we can guarantee at runtime that an instance of the needed service will be available in the pool.
Implementation
In the reporting project:
- We add an interface to the Document class:
package reporting;
public interface Report {
}
- We add an interface to the Printer class:
package utils;
import reporting.Report;
public interface PrinterUtils {
void print(Report report);
}
- And finally, modify the ReportGenerator class as follows:
package reporting;
import utils.PrinterUtils;
public class ReportGenerator {
private final PrinterUtils printer;
public ReportGenerator(PrinterUtils printer) {
this.printer = printer;
}
public void generate() {
Report report;
// ...
printer.print(report);
}
}
And notice here that there is no need for the reporting project to depend on the printer project anymore.
In the printer project:
- We make the class Document implement the interface Report provided by the reporting project:
package document;
import reporting.Report;
public class Document implements Report{
}
- We also make the Printer class implement the PrinterUtils interface provided by the reporting project:
package printer;
import reporting.Report;
import utils.PrinterUtils;
public class Printer implements PrinterUtils {
public void print(Report report) {
}
}
orchestrator
We also need an orchestrator project whose job is to start the reporting project providing it with the needed implementations. So this will depend on both reporting and printer projects.
public class Main {
public static void main(String[] args) {
PrinterUtils printer = new Printer();
ReportGenerator reportGenerator = new ReportGenerator(printer);
reportGenerator.generate();
}
}
After doing so, the compile time dependency graph would look like this
Outcome
After applying the Inversion of Control principle, our main project doesn't depend on external tools anymore. Hence, replacing the printer with another implementation of the same service requires a minimal change of code to be done at the level of the solution architecture.