The goal of this article is to provide an overview in simple terms of the concept of Inversion of Control (IoC), what it is, why it’s important, and a basic example of how it is implemented in a simple Java program consisting of a few classes and an interface.
At it’s most basic, the concept of IoC sets out to reverse the control between two classes via a third party (When developing modern Java applications this is usually Spring!). If we have two classes – a User class, and a Database class, the User class would directly depend on the Database class in order to save any data to the database. What if we change databases in the future and migrate away from MongoDB to Postgres? Sure, in a small application this wouldn’t be too difficult, but what about in an enterprise-level application with hundreds of thousands of lines of code and dozens of developers working on the same code base at the same time? We can do better! Enter Inversion of Control.
Before diving into an example, another key benefit to address regarding IoC is that without it, our application would be considered tightly coupled, and next to impossible to unit test. Why? Well, because in order to do so, you’d have to mock an entire database in the User/Database scenario from above, and that’s really not practical, nor is it in the spirit of unit testing. implementing IoC allows us to instead loosely couple our application and actually unit test our code without the need to mock an entire database.
First, let’s look at a tightly coupled example that does not implement IoC:
//User.java
public class User {
MongoDb database;
public User() {
database = new MongoDb();
}
public void add(String data) {
database.save(data);
}
}
//MongoDb.java
public class MongoDb {
public void save(String data) {
System.out.println("The data you provided has been saved! " + data);
}
}
A pretty straight forward example. When we create a new User object, we are creating an instance of a MongoDb object each and every time. That MongoDb instance comes with a save method that takes in a string and in this example just returns a mock string back, using the same data that was passed in.
In this example, performing a unit test is extremely difficult because our User objects directly rely on an instance of MongoDb! There’s really no way for us to pass in a mock instance of the database since it’s being created at the same time as the User object – hence the term tightly coupled.
Also, if in the future the decision is made to utilize another database, it’s not easily done since we’ve essentially hard-coded the User class to depend directly on the MongoDb class. All of the subsequent methods in the User class would have to be updated.
Let’s take a look at an example employing the IoC paradigm in which we pass in MongoDb as an argument to the User constructor… Sounds like unit testing would be a little easier if we’ve already been given a database! We’re going to reverse the flow of control and instead of the User class controlling the database, we’re going to tell it what to do by passing a pre-existing database into its constructor… We’re inverting the control!
//User.java
public class User {
MongoDb database;
public User(MongoDb database) {
this.database = database;
}
public void add(String data) {
database.save(data);
}
}
Every time a user Object is created, we’re passing in a pre-existing instance of a MongoDb instance! Unit testing can be carried out since we’re being given a database each time we create a User! (In unit testing we’re only testing a single unit of code – imagine having to mock a database and hope you got its implementation right so you don’t run into unexpected bugs?)
Let’s take the above example a step further. We’re on the right track in terms of abstracting away where a new instance of MongoDb is being created, thus allowing us to unit test our code. But what if our team decides to migrate away from MongoDB and use MySQL instead?
Here we go again! The User class is expecting a MongoDb instance to be passed in! Let’s fix this by implementing a generic database interface that will force its behavior on whatever class we implement it in. Remember: an interface is a contract of behavior, so where ever it is implemented, we are guaranteed to, at the very minimum, have access to whatever the interface defines.
Let’s create a generic interface that all databases will implement:
//Database.java
public interface Database {
void save(String data);
}
Where ever we implement this interface, we’re guaranteed to at least have a save method that doesn’t return anything, but takes in a String. Now let’s update our existing MongoDb class to implement this new interace:
//MongoDb.java
public class MongoDb implements Database {
public void save(String data) {
System.out.println("The data you provided has been saved! " + data);
}
}
The change is subtle, but this is good as we’re getting closer to a really robust solution. Now the MongoDb class implements the Database interface. It must have a save method that doesn’t return anything and takes in a string as an argument. If not, the code will not compile.
Finally, let’s update our User class to now rely on a generic Database object rather than a MongoDb class, a MySql class, Oracle class, etc.
//User.java
public class User {
Database database;
public User(Database database) {
this.database = database;
}
public void add(String data) {
database.save(data);
}
}
We can clearly see that our User class doesn’t really care about what kind of database is being passed in… So as long as it implements the Database interface, we are good to go and can utilize it in the class! We have achieved loose coupling and our program is much more robust than when we began our example.