Как отделить управляемый событиями модуль?
Может понадобиться немного предыстории, но перейдите к проблеме, если вы чувствуете себя уверенно. Надеюсь, что резюме даст понять смысл.
Резюме
у меня есть InputDispatcher
который отправляет события (мышь, клавиатура и т. д.) на Game
объект.
Я хочу масштабировать InputDispatcher
независимо от Game
: InputDispatcher
должен иметь возможность поддерживать больше типов событий, но Game
не следует заставлять их всех использовать.
Фон
Этот проект использует JSFML.
Входные события обрабатываются через Window
класс через pollEvents() : List<Event>
, Вы должны сделать отправку самостоятельно.
Я создал GameInputDispatcher
класс для отделения обработки событий от таких вещей, как обработка рамки окна.
Game game = ...;
GameInputDispatcher inputDispatcher = new GameInputDispatcher(game);
GameWindow window = new GameWindow(game);
//loop....
inputDispatcher.dispatch(window::pollEvents, window::close);
game.update();
window.render();
Цикл был упрощен для этого примера
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;
}
}
}
}
Эта проблема
В коде прямо выше (GameInputDispatcher
), Я мог бы отправлять события Game
создавая Game#onEvent(Event)
и звонит game.onEvent(event)
в случае по умолчанию.
Но это заставит Game
написать реализацию для сортировки и отправки событий мыши и клавиатуры:
class DemoGame implements Game {
public void onEvent(Event event) {
// what kind of event?
}
}
Вопрос
Если бы я хотел кормить события из InputDispacher
в Game
Как я могу это сделать, избегая нарушений Принципа разделения интерфейса? (объявив все методы прослушивания: onKeyPressed,
onMouseMoved , etc.. inside of
Game`, хотя они могут и не использоваться).
Game
должен быть в состоянии выбрать форму ввода, которую он хочет использовать. Поддерживаемые типы ввода (такие как мышь, клавиша, джойстик, ...) должны масштабироваться через InputDispatcher
, но Game
не следует заставлять поддерживать все эти входные данные.
Моя попытка
Я создал:
interface InputListener {
void registerUsing(ListenerRegistrar registrar);
}
Game
будет расширять этот интерфейс, позволяя InputDispatcher
зависит от InputListener
и позвонить registerUsing
метод:
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
теперь подтипы могут реализовывать любой слушатель, который поддерживается, затем зарегистрировать себя:
class DemoGame implements Game, MouseListener {
public void onKeyPressed(Keyboard.Key key) {
}
public void registerUsing(ListenerRegistrar registrar) {
registrar.registerKeyListener(this);
//...
}
}
Проблемы с попытками
Хотя это позволяет Game
подтипы для реализации только поведения, которое они хотят, это заставляет любого Game
объявить registerUsing
, даже если они не реализуют никаких слушателей.
Это можно исправить, сделав registerUsing
default
метод, позволяющий всем слушателям расширяться InputListener
переопределить метод:
interface InputListener {
default void registerUsing(ListenerRegistrar registrar) { }
}
interface MouseListener extends InputListener {
void registerUsing(ListenerRegistrar registrar);
//...listening methods
}
Но это было бы довольно утомительно для каждого слушателя, которого я выбрал, нарушая DRY.
2 ответа
Я не вижу смысла в registerUsing(ListenerRegistrar)
, Если должен быть написан код, внешний по отношению к слушателю, который знает, что это слушатель, и поэтому он должен зарегистрироваться в ListenerRegistrar
затем он может продолжить и зарегистрировать слушателя у регистратора.
Проблема, как указано в вашем вопросе, обычно решается в графическом интерфейсе с помощью обработки по умолчанию, с использованием наследования или делегирования.
С наследованием у вас будет базовый класс, назовите его DefaultEventListener
или же BaseEventListener
, что вы предпочитаете, который имеет public void onEvent(Event event)
метод, который содержит оператор switch, который проверяет тип события и вызывает переопределяемый для себя для каждого события, о котором он знает. Эти изменяемые обычно ничего не делают. Тогда ваша "игра" вытекает из этого DefaultEventListener
и обеспечивает переопределение реализаций только для событий, которые его волнуют.
С делегацией у вас есть switch
заявление, в котором вы проверяете на события, о которых вы знаете, и в default
пункт вашего switch
Вы делегируете некоторым defaultEventListener
типа DefaultEventListener
что, вероятно, ничего не делает.
Существует вариант, который объединяет оба: возвращение слушателей события true
если они обрабатывают событие, в этом случае switch
оператор сразу возвращается, так что событие больше не будет обрабатываться, или false
если они не обрабатывают событие, в этом случае switch
заявление break
s, так что код в конце switch
оператор берет на себя управление, и то, что он делает, это передает событие другому слушателю.
Альтернативный подход (используемый во многих случаях в SWT, например) включает регистрацию метода наблюдателя для каждого отдельного события, которое вы можете наблюдать. Если вы сделаете это, не забудьте отменить регистрацию каждого события, когда умирает ваш игровой объект, иначе он станет зомби. Приложения, написанные для SWT, полны утечек памяти, вызванных элементами управления графическим интерфейсом, которые никогда не собираются мусором, потому что у них где-то зарегистрирован наблюдатель, даже если они давно закрыты и забыты. Это также является источником ошибок, потому что такие элементы управления зомби продолжают получать события (например, события клавиатуры) и продолжают пытаться что-то делать в ответ, даже если у них больше нет графического интерфейса.
Повторяя проблему другу, я думаю, что нашел проблему.
Хотя это позволяет подтипам Game реализовывать только те поведения, которые они хотят, это заставляет любую Game объявлять registerUsing, даже если они не реализуют никаких слушателей.
Это предполагает Game
уже нарушает провайдера: если клиенты не будут использовать слушателей, Game
не должно происходить из InputListener
,
Если по какой-то причине Game
подтип не хотел использовать слушателей (возможно, взаимодействие осуществляется через веб-страницы или локальную машину), Game
не должен быть вынужден объявить registerUsing
,
Решение
Вместо этого InteractiveGame
может происходить из Game
и реализовать InputListener
:
interface Game { }
interface InteractiveGame extends Game, InputListener { }
Затем структура должна будет проверить тип Game
чтобы увидеть, нужно ли создавать InputDispatcher
:
Game game = ...;
if(game instanceof InteractiveGame) {
// instantiate input module
}
Если кто-то может предложить лучший дизайн, пожалуйста, сделайте это. Этот дизайн был попыткой отделить диспетчеризацию событий от программ, которые хотят использовать пользовательские события, обеспечивая при этом строгую безопасность во время компиляции.