To write code that is maintainable and readable, understanding of certain architectural and design principles is needed. Let’s start with a few concepts and apply them in an example using Spring framework. The terms discussed in this post are quite broad (with differing definitions, opinions and contexts), but our focus here is on addressing the problem of coupling between layers/tiers of a multi-tiered application (service layer, data access layer etc.).
Dependency Inversion Principle (DIP)
Dependency inversion principle is the ‘D’ in the SOLID principles of Object Oriented Design. DIP basically has two rules:
a) High level modules should not depend on low level modules – both should depend on abstractions.
b) Abstractions should not depend upon details – details should depend upon abstractions.
Let’s take an example of a UserProfileService, which calls UserProfileDAO. A client uses UserProfileService to access and update profile of a user. Here is the code without Dependency Inversion Principle:
1 2 3 4 5 6 |
public class UserProfileService { ... UserProfileDAOHibernateJPAImpl dao = new UserProfileDAOHibernateJPAImpl(); ... dao.getUserProfileById(id); } |
The above example is violating the rules of dependency inversion principle. The high level module (service layer) is directly instantiating an object of low level module (data access layer) and there is no abstraction. The layers are tightly coupled. If we want to use a different implementation for data access, all the layers are impacted. We cannot use or test a mock implementation for example (say, for unit test purposes, without hitting actual database). To fix this problem of coupling, we use Inversion of Control.
Inversion of Control (IoC)
At a high level, Inversion of Control (IoC) is a software architecture design in which the flow of control of a system is inverted compared to “normal” or procedural programming. There are different aspects of control that can be inverted. For example, an event driven flow from say, a GUI form, is an inversion of control as compared to procedural flow (as the method call is not initiated by our code calling another method call, but by a framework automatically triggering a method call based on occurrence of a event). However in the context of the example chosen, we are interested in the following:
Inverting the control of abstraction/interface. The user of an interface controls the interface instead of the implementers (step 1 for decoupling)
Inverting the control of creation/binding of objects of dependent implementation classes – also called Dependency Injection (step 2).
An Inversion of Control container is then used to centralize the assembling/wiring of the dependencies (step 3). These three steps help to decouple the tiers. We will use these three steps to decouple Service layer and Data access layer of our application.
We begin with step 1, which is also aligned to the principle of “Program to an interface, not an implementation”.
Program to an interface, not an implementation
We create an interface for data access layer and all implementations have to abide by the interface definition. During the course of application development and testing, we can have different implementations (mock implementation with hardcoded values; read from XML/JSON/CSV files, Hibernate JPA implementation etc.). The implementations will have to be be easily interchangeable. The higher level module (Service layer), in a sense, “owns” the abstraction (of DAO layer) and dictates any changes needed to the abstraction.
1 2 3 4 |
public interface UserProfileDAO { public UserProfile getUserProfileById(String id); public UserProfile saveUserProfile(UserProfile userProfile); } |
1 2 3 4 5 6 |
public class UserProfileService { ... UserProfileDAO dao = new UserProfileDAOHibernateJPAImpl(); ... dao.getUserProfileById(id); } |
Now, an abstraction/interface (UserProfileDAO) has been created for data access layer. The service layer code uses the abstraction, but we are still instantiating a implementation class in the service layer. So the dependency is still there. In Step 2, we apply Inversion of Control using Dependency Injection.
Dependency Injection
A dependency is an object that is used/needed in a class. In the above example, an implementation class object (e.g. UserProfileDAOHibernateJPAImpl) is a dependency for UserProfileService. Dependency Injection basically means passing of dependent objects in as parameters rather then hardcoding. The dependent object can be passed as parameter in constructor (“Constructor Injection”) or using a setter method (“Setter Injection”). In Constructor Injection, UserProfileService should provide a parameterized constructor, where desired implementation of UserProfileDAO can be passed as parameter.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class UserProfileService { ... UserProfileDAO dao; ... public UserProfileService(UserProfileDAO dao) { this.dao = dao; } ... public UserProfile getUserProfileById(String id) { return dao.getUserProfileById(id); } } |
In Setter Injection, UserProfileService should provide a setter method, where desired implementation of UserProfileDAO can be passed as parameter.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class UserProfileService { ... UserProfileDAO dao; ... public void setDao(UserProfileDAO dao) { this.dao = dao; } ... public UserProfile getUserProfileById(String id) { return dao.getUserProfileById(id); } } |
Regardless of whether constructor injection or setter injection is used, the user/client which calls UserProfileService has to create the desired data access layer implementation class object and pass on as parameter. For example, using constructor injection:
1 2 3 4 5 6 7 8 |
public class ClientApp { public static void main(String[] args) { ... UserProfileDAO dao = new UserProfileDAOHibernateJPAImpl(); UserProfileService userProfileService = new UserProfileService(dao); ... } } |
Now, the service layer code does not directly create DAO implementation object anywhere. It uses only the UserProfileDAO interface. The code works with any implementation DAO implementation class object that the AppClient passes as parameter. The service layer and data access layer are decoupled, however the client or the user (AppClient) of UserProfileService now has the dependency on data access layer implementation class. The dependency has merely shifted one step. This brings us to the step 3, which is to use an IoC Container.
IoC Container
An Inversion of Control container (or in our case, a Dependency Injection Container) is a program which creates and configures dependencies (i.e. implementation objects). We tell the details of application dependencies to the container (using a centralized/external config file(or annotations). Using the configuration information, the container creates a service layer object, creates data access layer object, wires the two and returns the service that can be used by a client. With this, all the implementation classes (at architectural layer/tier level) are referenced only in configuration provided to IoC container. The actual code uses only interfaces. We can now change implementation classes to be used, by just changing configuration, without necessarily needing a recompile. The client code now changes to something like this:
1 2 3 4 5 6 7 8 9 |
public class ClientApp { public static void main(String[] args) { ... UserProfileService userProfileService = SomeIoCContainer.getUserProfileService("config.xml"); ... userProfileService.getUserProfileById(id); ... } } |
So by using the principles of ‘programing to an interface’, dependency injection and an IoC Container (to assemble/wire dependencies), we have effectively decoupled the architectural tiers of an application. In Part 2, we will demonstrate this using Spring as the IoC container (or Dependency Injection Container).