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.
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 aListenerRegistrar, 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
DefaultEventListenerorBaseEventListener, whatever you prefer, which has apublic 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 thisDefaultEventListenerand provides overriding implementations only for the events that it cares about.With delegation you have a
switchstatement in which you check for the events that you know about, and in thedefaultclause of yourswitchyou delegate to somedefaultEventListenerof typeDefaultEventListenerwhich probably does nothing.There is a variation which combines both: event listeners return
trueif they process the event, in which case theswitchstatement immediately returns so that the event will not be processed any further, orfalseif they do not process the event, in which case theswitchstatementbreaks, so code at the end of theswitchstatement 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.