Java Event Programming – How to Ensure a Method Handles Each Event

designevent-programminggenericsjava

I have several types of events, for example:

abstract class Event {
    static class KeyPress extends Event { ... }
    static class KeyRelease extends Event { ... }
    static class KeyHold extends Event { ... }
    // ...
}

And many listeners which respond to some of the above events by registering them in an event handler. It looks like this currently:

abstract class AbstractListener {
    Set<Class<? extends Event>> eventTypes;
    protected abstract boolean respond(Event event);
}

class DownKeyListener exteneds AbstractListener {
    DownKeyListener () {
        // just prepares to register to receive these events. doesn't matter how exactly.
        eventTypes.add(KeyPress.class);
        eventTypes.add(KeyHold.class);
        //no KeyRelease e.g.
    }

    boolean respond(Event event) {
        if (event instanceof KeyPress)
            return handleKeyPress(event);
        else if (event instanceof KeyHold)
            return handleKeyHold(event);
        return false;
    }

    private boolean handleKeyPress(KeyPress e) { ... }
    private boolean handleKeyHold(KeyHold e) { ... }
}

What I don't like about this is that there is nothing forcing or checking at least the relation between the registered events and the checks for them and handling in the respond method. This keeps leading to developer bugs. it's also a lot of code with instanceofs for little benefit (I would say).

So I though about doing something "smart" like this: create a map between the event types and the handlers so each event that is registered for handling will have a handler:

abstract class AbstractListener {
    Map<Class<? extends Event>, Function<? extends Event, Boolean> map = new ...
    //             ^ doesn't ensure these event ^: are the same but at least
    //                                             that someone responds
    // to ensure same event i can do
    protected <T extends Event> void register(Class<T> event, Function<T, Boolean> function) {
        map.put(event, function);
    }

    protected abstract boolean respond(Event event);
}

And then:

class DownKeyListener exteneds AbstractListener {
    DownKeyListener () {
        Map.put(KeyPress.class, keyPressFunction);
        Map.put(KeyHold.class, keyHoldFunction);
    }

    boolean respond(Event event) {
        Function<? extends Event, Boolean> f = map.get(event.getClass());
        return f == null ? false : f.apply(event);
    }

    Function<KeyPress, Boolean> keyPressFunction = event -> ...;
    Function<KeyHold, Boolean> keyHoldFunction = event -> ...;
}

Of course this doesn't work because of generics. apply gives an error and I understand why

The method apply(capture#4-of ? extends Event) in the type Function<capture#4-of ? extends Event,Boolean> is not applicable for the arguments (capture#5-of ? extends Event)

I don't know how to get what I want working correctly. Some things I had in mind:

  • Cast the result of map.get to something that will ensure it works properly
  • Change the respond method to be generic protected abstract <T extends Event> boolean respond(T event); which give the similar error:
The method apply(capture#3-of ? extends Event) in the type Function<capture#3-of ? extends Event,Boolean> is not applicable for the arguments (T)

Anyone has a suggestion on achieving what I want in any way?

Best Answer

You could use a visitor pattern, like so:

abstract class Event {
    boolean accept(EventVisitor visitor);

    KeyPress extends Event {
        boolean accept(EventVisitor visitor)
        {
            return visitor.process(this);
        }
    }
    KeyRelease extends Event { ... }
    KeyHold extends Event { ... }
    // ...
}

abstract class EventVisitor
{
    boolean process(KeyPress event)
    {
        return false;
    }

    boolean process(KeyRelease event)
    {
        return false;
    }

    // One default process() method for each subclass of Event.

    boolean respond(Event event)
    {
        return event.accept(this);
    }
}

and then

class DownKeyListener extends EventVisitor
{
    void process(KeyPress event)
    {
        return handleKeyPress(event);
    }

    void process(KeyHold event)
    {
        return handleKeyHold(event);
    }
}

This solution ensures that

  1. You avoid all the instanceof needed to dispatch on the event's type.
  2. All events that are not explicitly handled by a visitor implementation have a default handling in the abstract class EventVisitor.