Search code examples
javajava-21java-sealed-type

How to handle different implementations of a sealed interface in a scalable way in Java?


I have a sealed interface Event and a domain object that contains a List. Since all the possible implementations of Event are known at compile time, I can process each Event based on its class. The problem is that I will have many implementations of Event, and I want an easy way to add new handlers for new events without modifying a lot of existing code.

Currently, I use the following approach:

List<Event> events;
List<Handler> handlers;

events.forEach(event -> handlers.forEach(handler -> handler.handle(event)));

public class MealEventHandler implements Handler {
    @Override
    void handle(Event event) {
        if (event instanceof MealEvent mealEvent) {
            // logic
        }
    }
}

However, I want something more scalable, like this(but it doesn't compile):

public interface Handler<T extends Event> {
    default void handle(Event event) {
        if (event instanceof T e) {
            realHandle(e);
        }
    }
    void realHandle(T event);
}

This way, I could have a handler that specifically works with a certain type of Event. But I’m not sure how to modify the code to achieve this and allow for easy addition of new handlers. How can I improve my design to make it both type-safe and flexible for adding new event handlers as the number of Event types grows?


Solution

  • First of all, it makes no sense to use sealed - it is working against your needs. This answer by Brian Goetz is very helpful in understanding the correct usage.

    You could use delegation based on the event class.

    public class DelegatingHandler implements Handler<Event> {
    
      private static final NotSupportedEvent NOT_SUPPORTED_EVENT = new NotSupportedEvent();
    
      private final Map<Class<? extends Event>, Handler<? extends Event>> delegates = new HashMap<>();
    
      @Override
      public void handle(Event event) {
        Class<? extends Event> eventClass = event.getClass();
        //handle unsupported type appropriately
        //this falls back to no op
        @SuppressWarnings("unchecked")
        Handler<Event> delegate = (Handler<Event>) delegates.getOrDefault(eventClass, NOT_SUPPORTED_EVENT);
        delegate.handle(event);
      }
    
      public void registerDelegate(Class<? extends Event> eventClass, Handler<? extends Event> delegate) {
        //consider how to handle duplicate keys, depending on the use case
        delegates.put(eventClass, delegate);
      }
    
      private static final class NotSupportedEvent implements Handler<Event> {
    
        @Override
        public void handle(Event event) {
          System.out.println(event.getClass().getSimpleName() + ": not supported");
        }
      }
    }
    

    Based on the event class, it will find the correct handler for this type of event and delegate the actual work to it. If a handler for the event is not registered, it defaults to no op.

    Example usage:

    DelegatingHandler handler = new DelegatingHandler();
    handler.registerDelegate(MealEvent.class, new MealEventHandler());