In the field of Object-Oriented Software Development, the SOLID principles stand as foundational guides, driving software systems toward modularity, loose coupling, maintainability, and robustness. The last of these principles, Dependency Inversion Principle (DIP), seems to be often mixed up with other popular concepts such as Inversion of Control and Dependency Injection.
While learning a new programming language, we often begin with a single class and method, focusing on mastering basic language features while learning data structures and algorithms. We get acquainted with Object-Oriented Programming (OOP) concepts like classes, objects, and inheritance at a basic level (e.g., Car is a Vehicle, Manager is an Employee, etc.). I have also observed while interviewing candidates that not only the freshers but even the experienced developers lack a deeper understanding of the nuances of Dependency Inversion Principle (DIP), Inversion of Control (IOC), and Dependency Injection (DI).
With this blog post, I have tried to present the intricate distinctions between these concepts, unraveling their nuances in simple terms so that even novice learners of Object-Oriented Programming will understand quickly.
The definition of the Dependency Inversion Principle tells us…
To understand how DIP, DI, and IOC differ and relate to each other, let’s take an example of calculating foreign exchange.
class ForexService {
public double getValue(double amount, String fromCurrency, String toCurrency) {
if ("USD".equals(fromCurrency) && "INR".equals(toCurrency)) {
return amount * 82.0;
} else {
//....
}
}
}
Nobody writes such code these days.
getValue
depends on the called method getExchangeRate
(dependency).class ForexService {
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * getExchangeRate(fromCurrency, toCurrency);
}
private double getExchangeRate(String fromCurrency, String toCurrency) {
if ("USD".equals(fromCurrency) && "INR".equals(toCurrency)) {
return amount * 82.0;
} else {
//....
}
}
}
Nobody writes such code anymore, but fresh learners can relate to this.
2. The next step is to create a separate class to handle all logic related to determining an exchange rate.
In this example the ForexService
is a high-level class/module that uses a low-level class/module ExchangeRateService
.
class ForexService {
private ExchangeRateService service = new ExchangeRateService();
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * service.getExchangeRate(fromCurrency, toCurrency);
}
}
class ExchangeRateService {
public double getExchangeRate(String fromCurrency, String toCurrency) {
if ("USD".equals(fromCurrency) && "INR".equals(toCurrency)) {
return 82.0;
} else {
//....
}
}
But now, there is a tight coupling between ForexService
and ExchangeRateService
. e.g., If we want to unit test ForexService, we must also provide ExchangeRateService.
3. So, the next step is to introduce an abstraction. The service provider creates an interface, and the high-level module ForexService
now uses that interface.
interface ExchangeRateServiceInterface {
double getExchangeRate(String currency);
}
public class ExchangeRateServiceImpl implements ExchangeRateServiceInterface {
public double getExchangeRate(String fromCurrency, String toCurrency) {
//
}
}
// --------- imports libraries containing above code
class ForexService {
private ExchangeRateServiceInterface service = new ExchangeRateServiceImpl();
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * service.getExchangeRate(fromCurrency, toCurrency);
}
}
We may now provide a mock implementation for unit testing.
However, in practice, exchange rate service won’t be that simple and must evolve over time. It might be reading exchange rates from a file, but now it needs to fetch it from a third-party service that provides live exchange rates. Or there might be multiple providers. I leave it to your imagination how complex it might become; the important point to understand here is – our high-level module ForexService
still depends on the low-level module ExchangeRateServiceImpl
.
How? The interface ExchangeRateServiceInterface
is provided/owned by ExchangeRateServiceImpl
. If ExchangeRateServiceImpl
changes in a way that requires ExchangeRateServiceInterface
to change, then our high-level module ForexService
would also need to change.
Those who have difficulty understanding what is that way that might force the interface to change think of a need to change the getExchangeRate
method to accept another parameter (whatever might be the reason); Because the implementation owns the interface, they are free to change it any way they like (kind of). And if this happens, ForexService
needs to change its code accordingly.
4. So, the next step is to invert the dependency. Make the abstraction/interface owned by a high-level module (or make it public and controlled by high-level module(s)). Implementors/Service Providers will follow the contract introduced by the abstraction. This way, we have achieved both the requirements of DIP…
ForexService
as well as low-level module ExchangeRateServiceImpl
both depend on the abstraction ExchangeRateServiceInterface
.ExchangeRateServiceInterface
doesn’t depend on the implementation details ExchangeRateServiceImpl
. Implementation depends on the abstraction here.We can think of Standards and Protocols. Communities, Committees working on standards and protocols define common APIs/contracts as interfaces; users write their code using these APIs; providers write implementations; users inject these implementations when they run their code.
e.g. JPA defines ORM APIs – Hibernate gives implementation; Java Collections API defines API for different collections – Java Collections implementation classes or Apache commons-collections gives implementations; Servlet Specifications give us Servlet APIs – Tomcat server gives implementation.
In our example, ForexService
can now change the actual implementation by instantiating the new ExchangeRateServiceInterface
implementation as and when required.
public class LiveExchangeRateService implements ExchangeRateServiceInterface {
public double getExchangeRate(String fromCurrency, String toCurrency) {
// fetches from third party service
}
}
class ForexService {
private ExchangeRateServiceInterface service = new LiveExchangeRateService();
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * service.getExchangeRate(fromCurrency, toCurrency);
}
}
Both the requirements of DIP have been fulfilled; can we say we have implemented DIP? Can we further improve the overall design?
Yes, you noticed it correctly! The ForexService
, our high-level module, is still creating instances of low-level modules – ExchangeRateServiceImpl
/LiveExchangeRateService
/MockExchangeRateService
etc.
It does not depend on those low-level modules, but it still needs to initialize them. In other words, our high-level module is still creating and initializing low-level modules. So, we need to get rid of this to achieve DIP fully.
We could say who else than the user (ForexService
) can better tell what it needs. You are absolutely right! But, from the user’s side, why should the user be responsible for initializing those dependencies? Why not make those available to the user, properly initialized, and ready to use? Also, think of complex examples where the number of dependencies is large and/or initializing dependencies is itself a complex process.
Here comes the Inversion of Control and Dependency Injection. We need to invert the control of creation and initialization of dependencies to someone other than these two parties (user and dependency). Once the dependencies have been initialized, we can make those available (inject) to the user.
To better understand IOC, the Command line vs. User Interface application is the best example. Consider a simple login flow to any application where a username and password are used for authentication.
The Command line app prompts for a username, we enter the username, then it prompts for a password, we enter the password, then it logs us in. The application is controlling the flow OR the application developer has control over the flow of the events.
On the other hand, the UI app presents us with UI elements where we can enter username and password any time, in any order. UI application code registers event handlers with the “controller” controlling the flow. This controller (mostly browser) decides to call the handler registered to an event or what thing to do next.
IOC introduces a “controller” that controls the flow on similar lines. We call it a framework or container as it acts as a container for various instances it creates (e.g., Spring, Angular frameworks). The framework creates dependencies and injects them into the users of those dependencies.
Another way to understand IOC is to think of a web application. In the case of a web application, the web server is in control of the flow of the events. Web application developer codes HTTP request handlers as dependencies of the web server. The web server reads the configuration where we map the request to its handler. The web server then initializes these dependencies and invokes handlers when a respective URL is requested.
As an example, below code shows how the controller/container/framework might be creating an instance.
class Container {
ExchangeRateServiceInterface createExchangeRateService(String type) {
switch (type) {
case "SIMPLE": return new ExchangeRateServiceImpl();
case "LIVE": return new LiveExchangeRateService();
case "MOCK": return new MockExchangeRateService();
default: throw IllegalArgumentException("unknown type");
}
}
}
This is the most fundamental illustration of creating instances. How the framework creates and initializes different instances varies by the framework. Spring, for example, provides ways to declare classes using annotations or configuration files so the framework can identify and initialize those. You can refer to the information available online about how IOC works and is implemented in various frameworks.
Once the IOC container creates the instance of a dependency, the next step is to make it available to the user of that dependency. IOC abstracts out the object creation logic. Dependency Injection gives us different ways to inject that object into other objects wherever it is required.
The below example shows constructor injection. The user (ForexService
) defines a constructor accepting an abstraction of the dependency (ExchangeRateServiceInterface
). Then the controller/ container/framework would initialize the User (ForexService
) with the appropriate instance of the dependency (e.g., ExchangeRateServiceImpl
) it has already created.
class ForexService {
private ExchangeRateServiceInterface service;
public ForexService(ExchangeRateServiceInterface service) {
this.service = service;
}
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * service.getExchangeRate(fromCurrency, toCurrency);
}
}
In the case of Setter Injection, a user (ForexService
) defines a setter method accepting an abstraction of the dependency. The controller/framework would create an appropriate instance of the service provider and call the setter method of ForexService
with that provider.
class ForexService {
private ExchangeRateServiceInterface service;
public void setForexService(ExchangeRateServiceInterface service) {
this.service = service;
}
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * service.getExchangeRate(fromCurrency, toCurrency);
}
}
Interface Injection is similar to the setter injection. The only additional thing is that the dependency user is required to implement an interface that has the definition of the setter method. So, the setter implementation is made mandatory for the user of the dependency.
interface ExchangeRateServiceUser {
public void setExchangeRateService(ExchangeRateServiceInterface service);
}
class ForexService implements ExchangeRateServiceUser {
private ExchangeRateServiceInterface service;
public void setExchangeRateService(ExchangeRateServiceInterface service) {
this.service = service;
}
public double getValue(double amount, String fromCurrency, String toCurrency) {
return amount * service.getExchangeRate(fromCurrency, toCurrency);
}
}
Below is an example of how the container might be injecting dependencies.
class Container {
ExchangeRateServiceInterface exchangeRateService;
ForexService forexService1, forexService2;
public void initialize(/*params*/) {
this.exchangeRateService = createExchangeRateService("SIMPLE"); // implementated above
forexService1 = new ForexService(this.exchangeRateService); // constructor injection
forexService2 = new ForexService();
forexService2.setExchangeRateService(exchangeRateService); // setter injection
}
}
Again, this is the crudest example of how dependency injection is done. How it is actually implemented varies from framework to framework. e.g., Spring framework supports setter as well as constructor injection. Also, the @Atowired
annotation doesn’t require us to define a constructor or setter method these days. You can refer to the online documentation of the framework for more information.
So, can we spot the differences?
DIP is a principle that focuses on high-level and low-level modules being independent of each other and establishing a contract between them using abstractions/interfaces. It’s then left to you to decide how to create and inject dependencies.
IOC is a mechanism to achieve inversion of control of object creation and initialization by handing it over to the third party (framework). Whereas,
DI is a technique to make these dependencies available to the user of those dependencies.
Also, observe the elegance of their collaboration to achieve modularity, loose coupling, maintainability, and robustness. They serve as the foundation for the development of many frameworks like Spring, Angular etc.
Inversion of Control Containers and the Dependency Injection pattern