CDI (Part 3): Events and Observers for Low Coupling and High Cohesion

Hello!

This is the Part 3 of the CDI Series in Java that contains:

  • Part 1: Factory in CDI with @Produces annotation
  • Part 2: CDI Qualifiers in Java: Polymorphism in DI
  • Part 3 : CDI Events and Observers for Low Coupling and High Cohesion
  • Part 4 : CDI and Lazy Initialization
  • Part 5 : Interceptors in CDI
  • Part 6 : CDI Dependency Injection and Alternatives
  • Part 7 : Working with CDI Decorators

In this part of the CDI Series , we'll explore Events and Observers used in CDI to have code with Low Coupling and High Cohesion .

Let's contextualize our application problem.

  • We must have a Checkout class
  • We must send an email when a checkout is finished
  • We must send a metric when a checkout is finished

Let's code!

Step 1: Creating the Entire Scenario

This first class will use CDI 2.0, which allows us to use CDI in a standalone mode:

public class MainApplication {

    public static void main(String[] args) {
        try (CDI<object> container = new Weld().initialize()) {
            Checkout checkout = container.select(Checkout.class).get();

            Buyer buyer = new Buyer("welcome@hackingcode.com");
            Order order = new Order(buyer, 10L, 80.0);

            checkout.finishCheckout(order);
        }
    }

}

This code won't compile, since we don't have the Order class yet.

Next, we have this Order class with a few fields:

public class Order {

    private double price;
    private Long id;
    private Buyer buyer;

    public Order(Buyer buyer, Long id, double price) {
        this.buyer = buyer;
        this.id = id;
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

    public Long getId() {
        return id;
    }

    public Buyer getBuyer() {
        return buyer;
    }

}

As you can see, an Order has a Buyer . Let's create this class:

public class Buyer {

    private String email;

    public Buyer(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

}

Great!

It's time to create the class that sends metrics . This is good cohesion, since this class is responsible only for sending metrics:

public class MetricCreator {

    public void createMetricFor(Order order) {
        System.out.println("Creating new Metric for OrderId: " + order.getId());
    }

}

Following the same thought, we'll create the EmailSender class:

public class EmailSender {

    public void sendEmailFor(Buyer buyer) {
        System.out.println("Sending email to: " + buyer.getEmail());
    }

}

Finally, it's time to create the Checkout class. In finishCheckout() , we're receiving an Order as a parameter and, with this object, we send an email and metrics :

public class Checkout {

    @Inject
    private EmailSender emailSender;

    @Inject
    private MetricCreator metrics;

    public void finishCheckout(Order order) {
        System.out.println("Finishing Checkout with Id: " + order.getId());

        emailSender.sendEmailFor(order.getBuyer());

        metrics.createMetricFor(order);
    }

}

Nice!

So far, we have good cohesion and low coupling because the Checkout class just knows the methods to send email and metrics but it doesn't know how .

But what if we need to do more? For example:

  • Send a push notification
  • Send Kafka messages for other systems
  • Send events to Amazon SQS for third-party integrations

Can we follow the same pattern as above? Can we just inject the responsible class in the Checkout class?

public class Checkout {

    @Inject
    private EmailSender emailSender;

    @Inject
    private MetricCreator metrics;

    @Inject
    private PushNotification pushNotification;    

    @Inject
    private KafkaMessager kafka;    

    @Inject
    private AmazonSqs amazonSqs;    

    public void finishCheckout(Order order) {
        System.out.println("Finishing Checkout with Id: " + order.getId());

        emailSender.sendEmailFor(order.getBuyer());

        metrics.createMetricFor(order);

        pushNotification.send(order);

        kafka.send(order.getId(), order.getBuyer());

        amazonSqs.send(order);
    }

}

As you can see, we can quickly fall into a bunch of injections that don't matter to the Checkout itself.

Every timewe need to use the Order in Checkout, we should open the Checkout class to add an @Inject annotation with the new object.

To follow good cohesion, in this case, I'd like to just see code related to a checkout being finished.

Let's refactor this code with more CDI!

Step 2: Refactoring to Use CDI Events and Observers

Do you remember the Observer Pattern ? So, we will use that to notify that one action has happened .

We'll create a class called CheckoutEvent that will indicate the checkout event.

public class CheckoutEvent {

    private Order order;

    public CheckoutEvent(Order order) {
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }

}

Now it's time to get help from CDI. We must use the Event Interface to notify all subscribers interested in the checkout event:

public class Checkout {

    @Inject
    private Event<checkoutevent> event;

    public void finishCheckout(Order order) {
        System.out.println("Finishing Checkout with Id: " + order.getId());

        event.fire(new CheckoutEvent(order));
    }

}

As you can see, we've used the method fire() to trigger the action to notify all subscribers in the event

Step 3: Observing the CDI Event

When a Checkout action is taken, we would like to have two actions executed:

  • Send an email
  • Create a metric

We should indicate that the EmailSender class is interested in the checkout event. We can do that by using the @Observes annotation from CDI, as you can see below:

public class EmailSender {

    public void sendEmailFor(@Observes CheckoutEvent event) {
        Buyer buyer = event.getOrder().getBuyer();

        System.out.println("Sending email to: " + buyer.getEmail());
    }

}

Of course, we can subscribe more interested classes, as we will see with the MetricCreator class below:

public class EmailSender {

    public void sendEmailFor(@Observes CheckoutEvent event) {
        Buyer buyer = event.getOrder().getBuyer();

        System.out.println("Sending email to: " + buyer.getEmail());
    }

}

Excellent! Notice that your Checkout class doesn't know about the EmailSender and MetricCreator classes. The finishCheckout() method is just worried about its logic and will notify everyone who is interested in this event, but without knowing them . Pretty cool!

That's it! I hope that this part of the CDI series is useful to you!

In the next post we're going to explore Lazy Initialization in CDI: Part 4: CDI and Lazy Initialization

Thanks!