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.