How to decouple an event-driven module?

680 Views Asked by At

A little background may be needed, but skip to Problem if you feel confident. Hopefully the summary gets the point across.

Summary

I have an InputDispatcher which dispatches events (mouse, keyboard, etc...) to a Game object.

I want to scale InputDispatcher independently of Game: InputDispatcher should be able to support more events types, but Game should not be forced to use all of them.

Background

This project uses JSFML.

Input events are handled through the Window class via pollEvents() : List<Event>. You must do the dispatching yourself.

I created a GameInputDispatcher class to decouple event handling from things such as handling the window's frame.

Game game = ...;
GameInputDispatcher inputDispatcher = new GameInputDispatcher(game);

GameWindow window = new GameWindow(game);

//loop....
inputDispatcher.dispatch(window::pollEvents, window::close);
game.update();
window.render();

The loop has been simplified for this example

class GameInputDispatcher {
    private Game game;

    public GameInputDispatcher(Game game) {
        this.game = game;
    }

    public void dispatch(List<Event> events, Runnable onClose) {
        events.forEach(event -> {
            switch(event.type) {
                case CLOSE: //Event.Type.CLOSE
                    onClose.run();
                    break;
                default:
                    // !! where I want to dispatch events to Game !!
                    break;
            }
        }
    }
}

The Problem

In the code directly above (GameInputDispatcher), I could dispatch events to Game by creating Game#onEvent(Event) and calling game.onEvent(event) in the default case.

But that would force Game to write the implementation for sorting & dispatching mouse and keyboard events:

class DemoGame implements Game {
    public void onEvent(Event event) {
        // what kind of event?
    }
}

Question

If I wanted to feed events from InputDispacher into Game, how could I do so while avoiding Interface Segregation Principle violations? (by declaring all listening methods: onKeyPressed,onMouseMoved, etc.. inside ofGame`, even though they may not be used).

Game should be able to choose the form of input it wants to use. The supported input types (such as mouse, key, joystick, ...) should be scaled through InputDispatcher, but Game should not be forced to support all these inputs.

My Attempt

I created:

interface InputListener {
    void registerUsing(ListenerRegistrar registrar);
}

Game would extend this interface, allowing InputDispatcher to depend on InputListener and call the registerUsing method:

interface Game extends InputListener { }

class InputDispatcher {
    private MouseListener mouseListener;
    private KeyListener keyListener;

    public InputDispatcher(InputListener listener) {
        ListenerRegistrar registrar = new ListenerRegistrar();
        listener.registerUsing(registrar);

        mouseListener = registrar.getMouseListener();
        keyListener = registrar.getKeyListener();
    }

    public void dispatch(List<Event> events, Runnable onClose) {
        events.forEach(event -> {
            switch(event.type) {
                case CLOSE:
                    onClose.run();
                    break;
                case KEY_PRESSED:
                     keyListener.onKeyPressed(event.asKeyEvent().key);
                     break;
                 //...
            }
        });
    }
}

Game subtypes can now implement whatever listener is supported, then register itself:

class DemoGame implements Game, MouseListener {
   public void onKeyPressed(Keyboard.Key key) {

   }

    public void registerUsing(ListenerRegistrar registrar) {
        registrar.registerKeyListener(this);
        //...
    }
}

Attempt Issues

Although this allows Game subtypes to only implement the behaviors they want, it forces any Game to declare registerUsing, even if they don't implement any listeners.

This could be fixed by making registerUsing a default method, having all listeners extend InputListener to redeclare the method:

interface InputListener {
    default void registerUsing(ListenerRegistrar registrar) { }
}

interface MouseListener extends InputListener {
    void registerUsing(ListenerRegistrar registrar);

    //...listening methods
}

But this would be quite tedious to do for every listener I choose to create, violating DRY.

2

There are 2 best solutions below

9
Mike Nakis On

I do not see any point in registerUsing(ListenerRegistrar). If code external to the listener must be written which knows that this is a listener and therefore it needs to register with a ListenerRegistrar, then it may as well go ahead and register the listener with the registrar.

The Problem as stated in your question is usually handled in GUIs is by means of default processing, using either inheritance or delegation.

With inheritance, you would have a base class, call it DefaultEventListener or BaseEventListener, whatever you prefer, which has a public void onEvent(Event event) method that contains a switch statement which checks the type of event and invokes an overridable on itself for every event that it knows about. These overridables generally do nothing. Then your "game" derives from this DefaultEventListener and provides overriding implementations only for the events that it cares about.

With delegation you have a switch statement in which you check for the events that you know about, and in the default clause of your switch you delegate to some defaultEventListener of type DefaultEventListener which probably does nothing.

There is a variation which combines both: event listeners return true if they process the event, in which case the switch statement immediately returns so that the event will not be processed any further, or false if they do not process the event, in which case the switch statement breaks, so code at the end of the switch statement takes control, and what it does is to forward the event to some other listener.

An alternative approach (used in many cases in SWT for example) involves registering an observer method for each individual event that you can observe. If you do this then be sure to remember to deregister every event when your game object dies, or else it will become a zombie. Applications written for SWT are full of memory leaks caused by gui controls that are never garbage-collected because they have some observer registered somewhere, even though they are long closed and forgotten. This is also a source of bugs, because such zombie controls keep receiving events (for example, keyboard events) and keep trying to do things in response, even though they have no gui anymore.

0
Vince On

While reiterating the issue to a friend, I believe I found the issue.

Although this allows Game subtypes to only implement the behaviors they want, it forces any Game to declare registerUsing, even if they don't implement any listeners.

This suggests Game is already violating ISP: if clients won't use listeners, Game should not derive from InputListener.

If, for some reason, a Game subtype did not want to use listeners (maybe interaction is handled via Web pages or the local machine), Game should not be forced to declare registerUsing.

Solution

Instead, an InteractiveGame could derive from Game and implement InputListener:

interface Game { }
interface InteractiveGame extends Game, InputListener { }

The framework would then have to check the type of Game to see if it needs to instantiate an InputDispatcher:

Game game = ...;
if(game instanceof InteractiveGame) {
    // instantiate input module
}

If someone can suggest a better design, please do so. This design was an attempt to decouple event dispatching from programs that want to make use of user events, while enforcing strong compile-time-safety.