The Holly Trinity: DIP, IoC, DI.
There are topics of discussion that arise from time to time. Some of the topics evolve, others end up in a odd loop in which it seems we end up arguing pretty much over the same thing without actually getting anywhere.
One of the topics that I face from time to time, that here will call as the holly trinity is the relationship between three related concepts. The holly trinity is then composed by Dependency Inversion Principle (DIP) or DIP, Inversion of Control (IoC) and Dependency Injection (DI).
Before we get into the three concepts maybe it would help to first understand the why. Why do I even bother to get into them? The answer is twofold. First these are important concepts to get right in the context of software engineering. Second, people seem to keep confusing them. Sometimes I hear someone talking about DIP when in fact they mean IoC. Sometimes, and pretty frequent, people calling IoC and DI interchangeably as if they were synonymous. Also because from a talk with a friend I was noticed that in technical interviews some people come with answers like. I don't use IoC, usually I prefer DI.
In this post we aim to explore the Holly Trinity a bit and try to shed a bit of light how the relate to each other and what are the ideas that distinguish them.
Dependency Inversion Principle
Lets start by taking a look into what Dependency Inversion Principle means.
Consider the following definition:
Dependency inversion principle is one of the principles on which most of the design patterns are build upon. Dependency inversion talks about the coupling between the different classes or modules. It focuses on the approach where the higher classes are not dependent on the lower classes instead depend upon the abstraction of the lower classes. The main motto of the dependency inversion is Any higher classes should always depend upon the abstraction of the class rather than the detail.
Lets look at an example that fails to apply this principle.
Consider the following example
public class App
{
public static void main( String[] args )
{
final PostgresClient client = new PostgresClient();
final UserRepository userRepository = new UserRepository(client);
userRepositoryPostgres1.save(new User("John", "Doe"));
}
}
Here we fail to apply DIP. Why? Notice that UserRepository fails to depend upon an abstraction. It is directly coupled with the PostgresClient which in this example case would represent a connector specific for Postgres. And is this a problem? Well not really if you never plan to use other database. Or if you just plan to use one database at the time. Imagine for a moment that you'll have the task to migrate one database to another. Now you would need to create a clone of this UserRepositoryPostgres to UserRepositoryMysql, or alternatively change the code to deal with both operations like in UserRepositoryMixed. I guess it is clear by now that the amount of duplicated code and the inflexibility of it (for example if you would like to temporarily disable the persistency on one of the databases you would need to change the code again). Can we do better? Indeed we do. The problem here arises from violating the principle of depending on an abstraction instead we are building our repository, in fact several of them, just because we are coupling the concept of repository with the concept of a specific/concrete database. In the following example we can see a UserRepository implementation that depends on an abstraction instead of a concrete database client
public interface DatabaseClient {
long execute(final String sqlStatement);
}
public class PostgresClient implements DatabaseClient {
//Specific implementation for postgres
long execute(final String sqlStatement);
}
public class MysqlClient implements DatabaseClient {
//Specific implementation for mysql
long execute(final String sqlStatement);
}
public class App
{
public static void main( String[] args )
{
final DatabaseClient postgresClient = new PostgresClient();
final DatabaseClient mysqlClient = new PostgresClient();
//User repository saves on postgres
final UserRepository userRepository = new UserRepository(postgresClient);
userRepositoryPostgres1.save(new User("John", "Doe"));
//User repository saves on mysql
userRepository = new UserRepository(mysqlClient);
userRepositoryPostgres1.save(new User("John", "Doe"));
}
}
At this point you may ask. Wait, what about the other guys from the holly trinity? Ok. Lets jump into the next concept.
Inversion of control Principle
To make sense of this principle it helps to understand two things. What is being controlled and what do we mean by inverting it. This principle deals with the control of execution flow. It tell us that we should invert the execution flow.
To make this principle more clear let us review an example which does not follow the principle.
class Button {
private boolean isClicked = false;
public void click() {
System.out.println("Button clicked.");
isClicked = true;
}
}
class UserInterface {
private Button button = new Button();
public void start() {
if (userWantsToClick()) { // Direct control of execution flow
button.click();
} else {
System.out.println("Button was not clicked.");
}
}
private boolean userWantsToClick() {
// Logic to detect user input
return true;
}
}
public class Main {
public static void main(String[] args) {
UserInterface ui = new UserInterface();
ui.start(); // Client code controls everything
}
}
This particular piece of code violates the inversion control principle since the higher component, in this case the UserInterface has direct control over the lower level component.
To respect the inversion of control principle we can rewrite this in the following way
// Callback contract for the click event, this enable us to invert the control of flow
interface OnClickListener {
void onClick();
}
// We define a button class that delegates the flow of execution
// to the lower level component, in this case the click event callback
class Button {
private OnClickListener clickListener;
public void setOnClickListener(OnClickListener listener) {
this.clickListener = listener;
}
public void click() {
System.out.println("Button clicked.");
if (clickListener != null) {
// Inversion of control: delegate execution
clickListener.onClick();
}
}
}
// UserInterface Implements the OnClickListener and its a higher level
// component that delegates into Button class the execution flow via the
// event handler
class UserInterface implements OnClickListener {
private Button button;
public UserInterface() {
button = new Button();
button.setOnClickListener(this); // Register the listener (IoC)
}
@Override
public void onClick() {
System.out.println("Button click event handled by UI.");
}
// Simulation of flow of execution which is controlled by Button class
public void simulateUserClick() {
button.click();
}
}
public class Main {
public static void main(String[] args) {
UserInterface ui = new UserInterface();
// Control is inverted here
ui.simulateUserClick();
}
}
At first the inversion of control principle can seem a bit odd. It may feel a bit weired and complex this way of thinking, and for simple programs actually it may be. This principle shines on large codebases. The advantage of this principle comes from the fact that higher level components do not need to know or control the lower level ones. This means that by delegating the control of execution to the lower level components, the higher level ones do not need to have specific knowledge/code to manage the lower level components. This means that replacing lower level components becomes easier as well as the testing of components. This means code more flexible which is very desirable as the codebase grows into the thousands or millions of lines of code.
Dependency Injection
The last of the trinity is a simple concept, however with far consequences regarding the design of our software. Put it simply and from wikipedia
In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally.
This basically means that we should avoid doing things like this
public class Human{
private Arm leftArm;
private Arm rightArm;
private Leg leftLeg;
private Leg rightLeg;
public Human(){
this.leftArm=new Arm();
this.rigthArm=new Arm();
this.leftLeg=new Leg();
this.rightLeg=new Leg();
}
}
The first obvious reason why we should do this is because initialization on the constructor forces us to depend on a concrete dependency instead of an abstraction. This directly violates the dependency inversion principle.
But its worse. By doing initialization on the constructor we loose the capability to compose Human
object in multiple ways. The code becomes rigid and by not being able to provide different dependencies we make the code difficult if not impossible to test.
To implement dependency injection we just need to delegate the injection of the dependencies to the higher level component. We can do this by passing arguments on the constructor or using setters for the purpose.
Dependency injection is a simple concept. There is a slight more complex semantic for this which is what we call of automatic dependency injection. Automatic dependency injection is a software pattern usually implemented via a framework that makes this process automatic.
The pattern implements discovery mechanisms and basically build the dependency tree of objects and is responsible to do automatic initialization of lower level components and injecting those into the higher level.
Examples of notable automatic dependency injection engines/frameworks are:
Summary
These three principles are all useful when dealing with large scale software development. They help develop mantainable code by improving testability, composability and helps managing complexity. As all abstractions there are good and bad use cases. These patterns are usually found on large codebases. For programs in the realm of a few hundreds of lines these patterns may be overkill. If you got only ten classes on your program, if the program long term mantainability is not an issue then integrating an automatic dependency injection engine may be too much. However these are widely applied patterns for large scale development for good reasons. Mantainability and code complexity is of utter importance when the code is big.