Making Readable Code With Dependency Injection and Jakarta CDI
Learn more about dependency injection with Jakarta CDI and enhance the effectiveness and readability of your code.
Join the DZone community and get the full member experience.
Join For FreeWithin programming, object orientation (also known as OOP) is one of the wealthiest paradigms in documentation and standards, as with dependency injection.
However, the most common is that these standards are misinterpreted and implemented and what would allow a clear vision and a good design for a cooperative application, in practice, harms the quality and efficiency of your code.
In this article, I will bring the advantages of exploring object orientation, more specifically dependency injection, together with the specification of the Java world that covers this API: the Jakarta CDI.
What Is Dependency Injection?
The first step is to contextualize what dependency injection is and why it differs from inversion of controls, although the two concepts are very confused.
In the context of OOP (Object Orientation), dependency is related to any direct subordination of a class, and that can be done in several ways. For example, through constructors, accessor methods, such as setters, or creation methods, such as the Factory Method.
public Person(String name) {
this.name = name;
}
This example injection also can measure how much one class is linked to another, which we call coupling. It is vital because it is part of the job of good code design to be concerned with high cohesion and low coupling.
The Difference Between Dependency Injection and Inversion
Once the basic concepts of OOP are explained, the next step is to understand what differs between injection and dependency inversion.
This distinction happens because of what we call the DIP, or Dependency Inversion Principle, which works as a guide to good practices focused on decoupling a class from concrete dependencies through two recommendations:
High-level modules must not depend on low-level modules, but both must depend on abstractions (Example: interfaces).
Abstractions, in turn, should not depend on details. However, parties or concrete implementations must depend on abstractions.
While inversion assumes all these requirements, dependency injection is a DIP technique to supply a class's dependencies, usually through a constructor, attribute or a method such as a setter.
In short: dependency injection is part of the best practices advocated by inversion of control (IoC).
As tricky as these concepts are, at first, they are fundamental because they can be correlated to the Barbara Liskov principle. After all, both the Liskov principle and dependency injection are part of SOLID.
Where Does CDI Come in, and How Do We Get to Jakarta CDI?
As with injection and inversion, it is crucial to take a step back and contextualize what CDI is.
The CDI (Dependency and Contexts Injection) arose from the need to create a specification within the JCP, JSR 365, mainly because, in the Java world and projects like Spring and Google Guice, there are several solutions and implementations of these frameworks.
Briefly, the purpose of Jakarta CDI is to allow the management of stateful or stateless component life control via context and component injection.
It is a project in constant evolution. If you want to keep up with the latest updates, it's worth knowing Eclipse Open-DI. It is currently being optimized to improve booting through CDI Lite.
CDI in Practice
To exemplify the features of CDI, we will create a simple Java SE application with CDI to show six essential elements within CDI, which are:
Perform a single injection.
Differentiate implementations through qualifications.
Teach CDI to create and distribute objects.
Apply Observer.
Apply Decorator.
Use the interceptor.
In this article, even to avoid making the text too big, we will highlight the codes. If you want, you can later access the code in full.
It is nice to mention that this content was created with Karina Varela and is part of the classes we took throughout 2021, mainly in American and European countries, in partnership with Microstream and Payara.
Perform a Single Injection
In our first implementation, we will create a "Vehicle" interface and an implementation using the Java SE container.
An important point: we have the context in addition to the injection of dependencies. We can define the life cycle of a class or bean, and, in this case, the implementation will have the application scope.
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
Vehicle vehicle = container.select(Vehicle.class).get();
vehicle.move();
Car car = container.select(Car.class).get();
car.move();
System.out.println("Is the same vehicle? " + car.equals(vehicle));
}
public interface Vehicle {
void move();
}
@ApplicationScoped
public class Car implements Vehicle {
//... implementation here
}
In this piece of code, we have the interface Vehicle and its respective implementation, Car
, in which the CDI will be able to inject an instance both through the interface and through the implementation.
Since we are talking about best practices, please understand that we are using the case of a single interface for a single implementation for teaching purposes. In practice, ideally, you don't do this so as not to break the KISS principle.
A good indicator for this is interfaces that start with “I” or implementations that end with “Impl”. In addition to being an indicator of unnecessary complexity, it is a code smell. After all, it is not a meaningful name, breaking the principle of the Clean Code.
Differentiate Implementations Through Qualifications
In the previous example, we had the case of a one-to-one relationship, that is, an interface for a single implementation. But what happens when we have multiple implementations for the same interface?
If we don't have this set, CDI won't know the default implementation and will throw the AmbiguousResolutionException. You can solve this problem using the Named annotation or a Qualifier. In our case, we will use the Qualifiers.
Imagine the following scenario in which we have an orchestra with several musical instruments. This orchestra will need to play all the instruments together and be able to discriminate between them. In our example, this scenario would look something like the following code:
<pre=”java”>
public interface Instrument {
String sound();
}
public enum InstrumentType {
STRING, PERCUSSION, KEYBOARD;
}
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MusicalInstrument {
InstrumentType value();
}
We have the instrument interface, an enumerator to define its type, and the CDI Qualifier through a new annotation in this code example.
After the implementations and qualifiers, it will be pretty simple for the orchestra to play all the instruments together and special tools considering their different types, for example, string, percussion, or wood.
@MusicalInstrument(InstrumentType.KEYBOARD)
@Default
class Piano implements Instrument {
@Override
public String sound() {
return "piano";
}
}
@MusicalInstrument(InstrumentType.STRING)
class Violin implements Instrument {
@Override
public String sound() {
return "violin";
}
}
@MusicalInstrument(InstrumentType.PERCUSSION)
class Xylophone implements Instrument {
@Override
public String sound() {
return "xylophone";
}
}
@ApplicationScoped
public class Orchestra {
@Inject
@MusicalInstrument(InstrumentType.PERCUSSION)
private Instrument percussion;
@Inject
@MusicalInstrument(InstrumentType.KEYBOARD)
private Instrument keyboard;
@Inject
@MusicalInstrument(InstrumentType.STRING)
private Instrument string;
@Inject
private Instrument solo;
@Inject
@Any
private Instance<Instrument> instruments;
}
The CDI container injects an Orchestra instance and demonstrates the use of each instrument.
Teach CDI to Create and Distribute Objects
In addition to defining its scope, it is often impossible to perform activities such as determining or creating a class within the CDI container. This type of resource is essential for cases such as, for example, when we want to inject a connection to some service.
It is possible to teach the container to create instances and destroy them within CDI through the Produces and Disposes annotations, respectively. For this example, we will create a connection that will be created and closed.
public interface Connection extends AutoCloseable {
void commit(String execute);
}
@ApplicationScoped
class ConnectionProducer {
@Produces
Connection getConnection() {
return new SimpleConnection();
}
public void dispose(@Disposes Connection connection) throws Exception {
connection.close();
}
}
Right now, we have a connection from which we teach how the CDI should create and close the connection.
Connection connection = container.select(Connection.class).get();
connection.commit("Database instruction");
Apply Observer
We cannot forget the Observer among the patterns very present in corporate architectures and present in the GoF.
One of the assessments I make of this pattern is that, given its importance, we can see it similarly in architectural practices, such as Event-Driven, and even in a paradigm with reactive programming.
In CDI, we can handle events synchronously and asynchronously. Imagine, for example, that we have a journalist, and he will need to notify all the necessary media. If we do the coupling of these media directly in the "Journalist" class, every time a media is added or removed, it will be necessary to modify it. It breaks the open-closed principle to solve; we will use Observer.
@ApplicationScoped
public class Journalist {
@Inject
private Event<News> event;
@Inject
@Specific
private Event<News> specificEvent;
public void receiveNews(News news) {
this.event.fire(news);
}
}
public class Magazine implements Consumer<News> {
private static final Logger LOGGER = Logger.getLogger(Magazine.class.getName());
@Override
public void accept(@Observes News news) {
LOGGER.info("We got the news, we'll publish it on a magazine: " + news.get());
}
}
public class NewsPaper implements Consumer<News> {
private static final Logger LOGGER = Logger.getLogger(NewsPaper.class.getName());
@Override
public void accept(@Observes News news) {
LOGGER.info("We got the news, we'll publish it on a newspaper: " + news.get());
}
}
public class SocialMedia implements Consumer<News> {
private static final Logger LOGGER = Logger.getLogger(SocialMedia.class.getName());
@Override
public void accept(@Observes News news) {
LOGGER.info("We got the news, we'll publish it on Social Media: " + news.get());
}
}
So we created a Journalist class that notifies the media, a novelty thanks to the Observer pattern with the CDI. The event is triggered by the Event
instance, and to listen for it; it is necessary to use the @Observers
annotation with the specific parameter to be listened to.
Apply Decorator
The Decorator pattern allows us to add behavior inside the object, obeying the principle of composition over inheritance. We see this within the Java world with wrappers of primitive types like Integer, Double, Long, etc.
public interface Worker {
String work(String job);
}
@ApplicationScoped
public class Programmer implements Worker {
private static final Logger LOGGER = Logger.getLogger(Programmer.class.getName());
@Override
public String work(String job) {
return "A programmer has received a job, it will convert coffee in code: " + job;
}
}
@Decorator
@Priority(Interceptor.Priority.APPLICATION)
public class Manager implements Worker {
@Inject
@Delegate
@Any
private Worker worker;
@Override
public String work(String job) {
return "A manager has received a job and it will delegate to a programmer -> " + worker.work(job);
}
}
We created a worker abstraction, the Programmer, and the Manager responsible for delegating the worker. In this way, we could add a behavior, such as sending an email, without modifying the programmer.
Worker worker = container.select(Worker.class).get();
String work = worker.work("Just a single button");
System.out.println("The work result: " + work);
The Interceptor
The CDI can also perform and control some operations in the code in a transversal way, similar to what we do with aspect-oriented programming and cutpoints with Spring.
The CDI interceptor tends to be quite helpful when we want, for example, a logging mechanism, transaction control, or a timer for a method that will be executed, among others. In this case, the sample used will be a timer with an interceptor.
@InterceptorBinding
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Timed {
}
@Timed
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class TimedInterceptor {
private static final Logger LOGGER = Logger.getLogger(TimedInterceptor.class.getName());
@AroundInvoke
public Object auditMethod(InvocationContext ctx) throws Exception {
long start = System.currentTimeMillis();
Object result = ctx.proceed();
long end = System.currentTimeMillis() - start;
String message = String.format("Time to execute the class %s, the method %s is of %d milliseconds",
ctx.getTarget().getClass(), ctx.getMethod(), end);
LOGGER.info(message);
return result;
}
}
The interceptor implementation will allow the creation of the annotation that will be used to indicate the intercept, and the class, in turn, will define how this intercept will be implemented, which, in our case, will be a counter.
The following and last example is to create two methods that we will count, with two implementations, one of which will have a delay of two seconds.
public class FastSupplier implements Supplier<String> {
@Timed
@Override
public String get() {
return "The Fast supplier result";
}
}
public class SlowSupplier implements Supplier<String> {
@Timed
@Override
public String get() {
try {
TimeUnit.MILLISECONDS.sleep(200L);
} catch (InterruptedException e) {
//TODO it is only a sample, don't do it on production :)
throw new RuntimeException(e);
}
return "The slow result";
}
}
Supplier<String> fastSupplier = container.select(FastSupplier.class).get();
Supplier<String> slowSupplier = container.select(SlowSupplier.class).get();
System.out.println("The result: " + fastSupplier.get());
System.out.println("The result: " + slowSupplier.get());
Dependency Injection With Jakarta CDI: More Options for Your Development
Given these practical examples, we can see the various possibilities and challenges possible when working with Object Orientation and better understand some of its principles around dependency injection and CDI.
It's important to remember that, as Uncle Ben once said, "with great power comes great responsibility." As such, common sense remains the best compass for exploring these and other CDI features, resulting in a high-quality design and clarity.
References
Opinions expressed by DZone contributors are their own.
Comments