In this article, we will find something out about Dependency Inversion Principle. Let’s get started.
Table of contents
- What is Dependency
- Dependency Inversion Principle
- Introduction to Dependency Injection
- Inversion of Control
- Dependency Injection with CDI of Java EE
- Advantages
- Benefits of SOLID code
- Wrapping up
What is Dependency
In fact, in object-oriented programming all this can be summarized by classes depending on other classes. Whenever class A uses another class B, then it is said that A depends on B. A can not work without B, and A can not be reused without also reusing B. In such a situtation, the class A is called a dependent
, and the class B is called a dependency
.
These classes are coupled either strongly or loosely, but let’s see a concrete example.
Here we have a class, BookService
, whose job is to create books. A book is represented by a book
class that contains the title of the book and a number. This number is actually an ISBN number generated by an IsbnGenerator
class, which has a method called generateNumber()
.
In the above diagram, BookService
depends on an IsbnGenerator
to create a book. Without an ISBN, the book could not be created. This dependency between classes is typical in object-oriented design. Classes have separate concerns.
-
Strongly coupled dependencies
Two classes that use each other are called coupled. Decoupling between classes can be loose or tight. Tight coupling leads to strong dependencies between classes. In an above example, IsbnGenerator is a class that has a unique method, generateNumber(), that returns an ISBN as a string. The simplicity will develop a very simple algorithm that generates a random number, starting by 13.
public class IsbnGenerator { public String generateNumber() { return "13-84356-" + Math.abs(new Random().nextInt()); } }
On the other hand, the BookService class is in charge of creating a book object. The createBook() method takes a title as a parameter and returns a Book object.
public class BookService { private IsbnGenerator isbn = new IsbnGenerator(); public Book createBook(String title) { return new Book(title, isbn.generateNumber()); } }
To complete, the
Book
object needs the title, as well as an ISBN number, and for that it delegates the work to theIsbnGenerator
class. As we can see, there is a strong dependency between those two classes. The BookService class depends on theIsbnGenerator
class, but what’s wrong with that?This type of depdency on the
IsbnGenerator
class means that BookService is only capable of creating books with ISBN numbers. It cannot use any other number generator, if needed. We can say that BookService is tightly coupled to theIsbnGenerator
class and thereby the number generator algorithm. It shows the strong coupling between classes can be bad because it decreases reuse. Remember that in OOP code, reuse is the idea that a class written at one time can be used by another class written at a later time.Strong coupling reduces reusability and, therefore, development speed, code quality, code readability, and so forth.
-
Loosely coupled Dependencies
A less tightly couplied solution would help in changing the NumberGenerator implementation at runtime. A way of doing it is through interfaces. Instead of depdending on the IsbnGenerator class, the BookService could depend on a NumberGenerator interface. This interface has one method called generateNumber() and is implemented by IsbnGenerator. If we need to generate ISSN numbers, we just create a new class called IssnGenerator that implements a different NumberGenerator algorithm. The BookService ends up depending on either an IsbnGenerator or an IssnGenerator according to some conditions or environment.
In terms of code, it’s quite easy. Everything starts with a Number Generator interface that defines a
generateNumber()
method. This interface is implemented by theIsbnGenerator
, which defines its ownNumberGenerator
algorithm, here a random number with a prefix starting with 13. To have a different implementation, we create a new class that implements a sameNumberGenerator
interface and redefines a differentNumberGenerator
algorithm, this time a number starting with 8.public interface NumberGenerator { String generateNumber(); } public class IsbnGenerator implements NumberGenerator { public String generateNumber() { return "13-84356-" + Math.abs(new Random().nextInt()); } } public class IssnGenerator implements NumberGenerator { public String generateNumber() { return "8-" + Math.abs(new Random().nextInt()); } }
Now that the classes are not directly coupled, how would we connect a
BookService
to either an ISBN or ISSN implementation? One solution is to pass the implementation to the constructor and leave an external class to choose which implementation is wants to use. So let’s refactor ourBookService
.public class BookService { private NumberGenerator generator; public BookService(NumberGenerator generator) { this.generator = generator; } public Book createBook(String title) { return new Book(title, generator.generateNumber()); } } BookService service = new BookService(new IsbnGenerator());
The
BookService
depends on an interface, not implementation. The implementation is passed as parameter of the constructor. So if we need aBookService
that generates an ISBN number, we just pass theIsbnGenerator
implementation to the constructor. If we need to generate an ISSN number, we just change the implementation to beIssnGenerator
. This is what’s calledInversion of Control
. The control of choosing the dependency is inverted because it’s giving to an external class, not the class itself. But we ends up connecting the dependencies ourselves using the constructor to choose implementation. This is calledConstructor injection
. Our techniques can be used, but all-in-all is just constructing dependency programmatically by hand, which is not flexible. Instead of constructing depedencies by hand, we can leave an injector to do it by using some frameworks such as CDI of Java EE, Dependency Injection of Spring framework, or something else.
Dependency Inversion Principle
The dependency Inversion Principle states that:
1. High level modules should not depend on low level modules; both should depend on abstractions.
2. Abstractions should not depend on details. Details should depend upon abstraction.
With the definitions of DIP, we have some questions such as What is high-level module or a low-level module?
-
High level modules
High level modules are the part of our application that bring real value. They are the modules written to solve real problems and use cases.
They are more abstract and map to the business domain. Most of us call this business logic. Each time we hear the words business logic, we are referring to those high-level modules that provide the features of our application. High level modules tell us what the software should do, not how it should do, but what the software should do.
-
Low level modules
Low level modules are implementation details that are required to execute the business policies. Because high-level modules tend to be more abstract in nature, at some point in time, we will need some concrete features that help us to get our business implementation ready. They are the plumbing for the internals of a system. And they tell us how the software should do various tasks. So, high level modules tell us what the software should do, and low level modules tell us how the software should do various takss.
For example, logging, data access, network communication, and IO.
-
Abstraction
Something that is not concrete.
Something that we can not “new” up. In Java applications, we tend to model abstractions using interfaces and abstract classes.
Let’s take a look at how this principle actually works. Traditionally, when we depend on details, our components tend to look like this. We have high level components, which directly depend upon low level components. Of course, this violates the dependency inversion principle because both should depend on abstractions.
Component A, which is a high level component, no longer depends directly on component B. It depends upon an abstraction. And component B, which is low level, also depends upon that abstraction.
For example,
// low level class
// It's a concrete class that use SQL to return products from the database.
class SqlProductRepo {
public Product getById(String productId) {
// grab product from SQL database
}
}
// High level class
class PaymentProcessor {
public void pay(String productId) {
SqlProductRepo repo = new SqlProductRepo();
Product product = repo.getById(productId);
this.processPayment(product);
}
}
We can easily find that PaymentProcessor has a direct dependency with the SqlProductRepo. Because in pay() method, we actually instantiate the repo of SqlProductRepo. We are newing up a new instance of the SqlProductRepo class. This clearly violates the dependency inversion principle. We will refactor to make this code better.
interface ProductRepo {
Product getById(String productId);
}
// low level class depends on abstraction
class SqlProductRepo implements ProductRepo {
@Override
public Product getById(String productId) {
// concrete details for fetching a product
}
}
class PaymentProcessor {
public void pay(String productId) {
ProductRepo repo = ProductRepoFactory.create();
Product product = repo.getById(productId);
this.processPayment(product);
}
}
class ProductRepoFactory {
public static ProductRepo create(String type) {
if (type.equals("mongo")) {
return new MongoProductRepo();
}
return new SqlProductRepo();
}
}
Now, our pay() method does not directly depend on a concrete implementation of the ProductRepo. We depend on the abstraction. We depen upon the ProductRepo interface. The factory will give us a concrete instance. It can be a SqlProductRepo, a MongoProductRepo, or an ExcelProductRepo. It doesn’t really matter, and this high level component doesn’t care what instance is served at runtime as long as it respects the contract. The factory is pretty simple. It has a static method that returns an instance of a ProductRepo abstraction.
Introduction to Dependency Injection
Dependency Injection is very used in conjunction with the Dependency Inversion Principle. However, they are not the same thing. Let’s look at how we left the PaymentProcessor class.
We have the pay() method, and the ProductRepo abstraction is now produced by the ProductRepoFactory. Although we have eliminated the coupling with the concrete SqlProductRepo class, we still have a small coupling with the ProductRepoFactory. We have more flexibility after applying the dependency inversion principle, but we can do a better design than this. Let’s come up with a better solution. This is where our dependency injection comes in.
-
Dependency Injection
Dependency injection is a technique that allows the creation of dependent objects outside of a class and provides those objects to a class.
We have various methods of doing this. One of them is by using public setters to set those dependencies. However, this is not a good approach because it might leave objects in an uninitialized state. A better approach is to declare all the dependencies in the component’s constructor like we are doing like the following.
class PaymentProcessor { public PaymentProcessor(ProductRepo repo) { this.repo = repo; } public void pay(String productId) { Product product = this.repo.getById(productId); this.processPayment(product); } } ProductRepo repo = ProductRepoFactory.create(); PaymentProcessor paymentProc = new PaymentProcessor(repo); paymentProc.pay("123");
-
Types of Dependency Injection
Dependency Injection can be performed by using:
-
Constructor injection
For example, with using CDI container.
public class DataUtil { @Produces private RealData data = new RealData(); } public class Notification { private RealData realData; @Inject public Notification(RealData realData) { this.realData = realData; } // ... }
-
Field injection
For example, with using CDI container.
public class Notification { @Inject private RealData data; }
-
Method injection
For example, with using CDI container.
public class Notification { private RealData data; @Inject public void setData(RealData data) { this.data =data; } }
-
Inversion of Control
Inversion of Control can help us create large system by taking away the reponsibility of creating objects.
Inversion of control is a design principle in which the control of object creation, configuration, and lifecycle is passed to a container or framework.
The control of creating and managing objects is inversed from the programmer to this container. We do not have to new up objects anymore. Something else creates them for us, and that something else is usually called an IoC container or DI container. The control of object creation is inverted. It’s not the programmer but the container that controls those objects. It makes sense to use it for some objects in an application like services, data access, or controllers.
However, for entities, data transfer objects, or value objects, it doesn’t make sense to use an IoC container. We can simply new up those objects, and it’s perfectly OK from architectural point of view.
There are many benefits in using an IoC container for our system.
- First of all, it makes it easy to switch between different implementations of a particular class at runtime.
- Then, it increases the programs modularity.
- Last but not least, it manages the lifecycle of objects and their configuration.
For example, at the core of the Spring framework is the Spring IoC container. Spring beans are objects used by our application and that are managed by the Spring IoC container. They are created with the configuration that we supply to the container.
There are many ways to configure an IoC container in Spring. XML is one example.Creating configuration classes is another. Or simply by annotating classes with special annotations like @Service, @Component, @Repository, …
@Configuration
public class DependencyConfig {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
return new B();
}
@Bean
public C c(A a, B b) {
return new C(a, b);
}
}
Dependency Injection with CDI of Java EE
-
Setup library to use CDI in our Java project
CDI is a standard dependency injection framework included in Java EE 6 and later.
It allows us to manage the lifecycle of stateful components via domain-specific lifecycle contexts and inject components (services) into client objects in a type-safe way.
-
Use javaee-api with version 8.0
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency>
To understand about Java EE 8, we can read deeper in this link.
To migrate a Java EE 8 project to Jakarta EE 8, replace the following dependency:
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency>
…with Jakarta EE 8 API
<dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>8.0.0</version> <scope>provided</scope> </dependency>
-
Use CDI 2.0
<dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency>
-
-
Implementation with CDI
-
Use qualifier in CDI
Context independency injection is a standard solution that manages dependency between classes. Injection is made using strongly type annotations, as well as XML configuration if needed. CDI removes boilerplate code by using a very simple API, so we do not have to use construction of dependencies by hand, and CDI brings many other features to dependency injection. To see how this works, let’s take back our example.
Nothing has changed in the above code. What changes is the way
BookServices
manages its dependencies. Basically it use@Inject
annotation from CDI to inject the implementation of theNumberGenerator
. This leaves the constructor useless, and we can just get rid of it. That means that the way of instantiatingBookService
has also changed. Instead of calling its constructor, we also need to inject it with CDI. Then to switch implementations, we use annotations and this way get aThirteenDigits
NumberGenerator, anEightDigits
NumberGenerator or any other one.// Use qualifier to specify which beans will be chosen @Qualifier @Retention(RUNTIME) @Target({ FIELD, TYPE, METHOD, PARAMETER }) public @interface ThirteenDigits { } public class BookService { @Inject @ThirteenDigits private NumberGenerator generator; public Book createBook(String title) { return new Book(title, generator.generateNumber()); } } @Inject BookService bookService;
CDI is a managed environment where the container uses a type-safe approach to inject the right dependency.
-
Use
@Named
annotationBeside the way that uses qualifier to differentiate many beans with same type, CDI allows us to perform service injection with the
@Named
annotation. This method provides a more semantic way of injecting services, by binding a meaningful name to an implementation:@Named("GiffFileEditor") public class GiffFileEditor implements ImageFileEditor { // ... } @Named("JpgFileEditor") public class JpgFileEditor implements ImageFileEditor { // ... } @Named("PngFileEditor") public class PngFileEditor implements ImageFileEditor { // ... }
At the moment, we will inject one of above beans in ImageFileProcessor class with constructor injection, or field injection, or method injection.
public class ImageFileProcessor { // field injection // @Inject // private @Named("PngFileEditor") ImageFileEditor editor; @Inject public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor editor) { // ... } // @Inject // public void setEditor(@Named("PngFileEditor") ImageFileEditor editor) { // this.editor = editor; // } }
-
Advantages
- Loose coupling
- Easier testing
- Better layering
- Interface-based design
- Dynamic proxies (segue to AOP)
Benefits of SOLID code
This is the last section of SOLID principles. So, we will conclude benefits when using SOLID in our code.
-
Easy to understand and reason about.
-
Changes are faster and have a minimal risk level.
-
Highly maintainable over long periods of time.
-
Cost effective.
Wrapping up
Thanks for your reading.
Refer:
SOLID Software Design Principles in Java
https://keyholesoftware.com/2014/02/17/dependency-injection-options-for-java/