Mastering Spring 5.0
上QQ阅读APP看书,第一时间看更新

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.

A piece of code (a method, a group of methods, or a class) is testable when you can easily write a simple unit test for it. One of the approaches used in unit testing is to mock the dependencies. We will discuss mocking in more detail later.

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();
Using interfaces helps in creating loosely coupled code. We can replace the wire with any implementation of an interface into a well-defined dependency.

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;
}
}
Instead of creating a setter for the data service, we could have also created a BusinessServiceImpl constructor accepting a data service as an argument. This is called a constructor injection.

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.