Understanding dependencies
We will start with writing a simple example; a business service talking to another data service. Most Java classes depend on other classes. These are called dependencies of that class.
Take a look at an example class BusinessServiceImpl, as follows:
public class BusinessServiceImpl {
public long calculateSum(User user) {
DataServiceImpl dataService = new DataServiceImpl();
long sum = 0;
for (Data data : dataService.retrieveData(user)) {
sum += data.getValue();
}
return sum;
}
}
Typically, all well-designed applications have multiple layers. Every layer has a well-defined responsibility. The business layer contains the business logic. The data layer talks to the external interfaces and/or databases to get the data. In the preceding example, the DataServiceImpl class gets some data related to the user from the database. BusinessServiceImpl class is a typical business service, talking to the data service DataServiceImpl for data and adding business logic on top of it (in this example, the business logic is very simple: calculate the sum of data returned by the data service).
BusinessServiceImpl depends on DataServiceImpl. So, DataServiceImpl is a dependency of BusinessServiceImpl.
Focus on how BusinessServiceImpl creates an instance of DataServiceImpl.
DataServiceImpl dataService = new DataServiceImpl();
BusinessServiceImpl creates an instance by itself. This is tight coupling.
Think for a moment about unit testing; how do you unit test the BusinessServiceImpl class without involving (or instantiating) the DataServiceImpl class? It's very difficult. One might need to do complicated things such as reflection to write a unit test. So, the preceding code is not testable.
Here's a question to think about: how do we make the preceding code testable? How do we reduce tight coupling between BusinessServiceImpl and DataServiceImpl?
The first thing we can do is to create an interface for DataServiceImpl. Instead of using the direct class, we can use the newly created interface of DataServiceImpl in BusinessServiceImpl.
The following code shows how to create an interface:
public interface DataService {
List<Data> retrieveData(User user);
}
Let's update the code in BusinessServiceImpl to use the interface:
DataService dataService = new DataServiceImpl();
For example, consider a business service that needs some sorting.
The first option is to use the sorting algorithm directly in the code, for example, bubble sort. The second option is to create an interface for the sorting algorithm and use the interface. The specific algorithm can be wired in later. In the first option, when we need to change the algorithm, we will need to change the code. In the second option, all that we need to change is the wiring.
We are now using the DataService interface, but BusinessServiceImpl is still tightly coupled as it is creating an instance of DataServiceImpl. How can we solve that?
How about BusinessServiceImpl not creating an instance of DataServiceImpl by itself? Can we create an instance of DataServiceImpl elsewhere (we will discuss who will create the instance later) and give it to BusinessServiceImpl?
To enable this, we will update the code in BusinessServiceImpl to have a setter for DataService. The calculateSum method is also updated to use this reference. The updated code is as follows:
public class BusinessServiceImpl {
private DataService dataService;
public long calculateSum(User user) {
long sum = 0;
for (Data data : dataService.retrieveData(user)) {
sum += data.getValue();
}
return sum;
}
public void setDataService(DataService dataService) {
this.dataService = dataService;
}
}
You can see that BusinessServiceImpl can now work with any implementation of DataService. It is not tightly coupled with a specific implementation: DataServiceImpl.
To make the code even more loosely coupled (as we start writing the tests), let's create an interface for BusinessService and have BusinessServiceImpl updated to implement the interface:
public interface BusinessService {
long calculateSum(User user);
}
public class BusinessServiceImpl implements BusinessService {
//.... Rest of code..
}
Now that we have reduced coupling, one question remains still; who takes the responsibility for creating instance of the DataServiceImpl class and wiring it to the BusinessServiceImpl class?
That's exactly where the Spring IoC container comes into the picture.