Как отделить модуль, управляемый событиями?

Может потребоваться небольшая предыстория, но если вы уверены, переходите к Проблеме. Надеюсь, краткое изложение уловит суть.

Резюме

У меня есть 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 ofGame`, даже если они не используются).

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.


person Dioxin    schedule 03.02.2017    source источник


Ответы (2)


Не вижу смысла в registerUsing(ListenerRegistrar). Если должен быть написан код, внешний по отношению к слушателю, который знает, что это слушатель, и поэтому ему необходимо зарегистрироваться с помощью ListenerRegistrar, то он может также пойти дальше и зарегистрировать слушателя у регистратора.

Проблема, указанная в вашем вопросе, обычно обрабатывается в графическом интерфейсе с помощью обработки по умолчанию с использованием либо наследования, либо делегирования.

При наследовании у вас будет базовый класс, назовите его DefaultEventListener или BaseEventListener, как вам больше нравится, который имеет метод public void onEvent(Event event), содержащий оператор switch, проверяющий тип события и вызывающий переопределяемый для себя для каждого события, о котором он знает. Эти переопределения обычно ничего не делают. Затем ваша «игра» происходит от этого DefaultEventListener и предоставляет переопределяющие реализации только для тех событий, о которых она заботится.

С делегированием у вас есть оператор switch, в котором вы проверяете известные вам события, а в предложении default вашего switch вы делегируете некоторым defaultEventListener типа DefaultEventListener, которые, вероятно, ничего не делают.

Существует вариант, сочетающий в себе оба варианта: прослушиватели событий возвращают true, если они обрабатывают событие, и в этом случае немедленно возвращается оператор switch, чтобы событие больше не обрабатывалось, или false, если они не обрабатывают событие, и в этом случае оператор switch breaks, поэтому код в конце оператора switch берет на себя управление и перенаправляет событие другому слушателю.

Альтернативный подход (используемый во многих случаях, например, в SWT) включает регистрацию метода наблюдателя для каждого отдельного события, которое вы можете наблюдать. Если вы сделаете это, не забудьте отменить регистрацию каждого события, когда ваш игровой объект умирает, иначе он станет зомби. Приложения, написанные для SWT, полны утечек памяти, вызванных элементами управления графического интерфейса, которые никогда не удаляются сборщиком мусора, потому что в них где-то зарегистрирован какой-то наблюдатель, даже если они давно закрыты и забыты. Это также является источником ошибок, потому что такие элементы управления-зомби продолжают получать события (например, события клавиатуры) и продолжают пытаться делать что-то в ответ, даже если у них больше нет графического интерфейса.

person Mike Nakis    schedule 03.02.2017
comment
registerUsing был способом скрыть Game информацию о его контейнерах (который InputDispatcher является одним из них). InputListenerRegistrar отделяет регистрацию от InputDispatcher. Это позволяет мне удалить замену InputDispatcher на все, что я хочу, поощряя повторное использование. - person Dioxin; 04.02.2017
comment
Что касается DefaultEventListener, я не хочу выполнять больше проверки типов, чем я уже делаю (что я делаю только потому, что JSFML в основном заставляет это делать). Использование этого метода по-прежнему нарушает правила ISP, поскольку неиспользуемые методы все равно будут объявлены. Несмотря на то, что DemoGame не нужно его объявлять, он все еще находится в интерфейсе DemoGame. Ничего не делающие методы сами по себе являются запахом кода. Я мог бы оставить registerUsing по умолчанию и не объявлять его повторно в KeyListener и MouseListener, если бы хотел такого поведения. - person Dioxin; 04.02.2017
comment
Вот как это делают несколько существующих графических интерфейсов. (Win32, MFC, WinForms, Swing, SWT, графический интерфейс, который я полностью построил сам и т. д.) Так что, нравится вам это или нет, вонючий или нет, это то, как многие люди, имеющие большой опыт работы с этими Дела решили это сделать. И вы знаете, как все думают, что они знают лучше, и они будут делать это по-другому? Ну, в этом случае они обычно не делают это по-другому. На это должна быть причина. Делайте что хотите с этой информацией. - person Mike Nakis; 04.02.2017
comment
Тогда ваш Game является производным от этого DefaultEventListener и предоставляет переопределяющие реализации только для тех событий, о которых он заботится. - Ни один инструментарий GUI не делает этого/не поощряет это. Я не хочу показаться нервным, но это плохой дизайн, переполняющий интерфейс шрифта (что является огромным табу). Следующее, что вы знаете, я бросаю UnsupportedOperationExceptions< /а>. Не могли бы вы привести пример, где вы это видели? - person Dioxin; 04.02.2017
comment
Посмотрите на это: msdn .microsoft.com/en-us/library/ Просмотреть все переопределяемые методы, начинающиеся с On...()? Они ничего не делают. Вы переопределяете их, если вам нужно что-то сделать. - person Mike Nakis; 04.02.2017
comment
Посмотрите, насколько огромен этот интерфейс. Это именно то, чего я пытаюсь избежать. Не могли бы вы привести пример, отличный от C++? Учитывая, что мой вопрос касается Java, два языка имеют два разных набора разработчиков, и не секрет, что разработчики Java, как правило, немного ближе в том, как они пишут код. И я действительно ненавижу подобное предубеждение, не поймите меня неправильно, но многословие C++ имеет тенденцию влиять на менталитет разработчиков. - person Dioxin; 04.02.2017
comment
Это не просто C++, это dotNet, поэтому на странице, которую вы видели, есть вкладка, которую вы можете использовать для выбора C#, C++, F#, VB. В частности, C# очень похож на java. Нет смысла пытаться избежать массивного интерфейса, потому что только один базовый класс фреймворка должен быть таким большим. Я повторяю: вы переопределяете только то, что хотите использовать. - person Mike Nakis; 04.02.2017
comment
Я хочу сказать, что могу добиться того же поведения, которое вы предлагаете, сделав registerUsing методом default. Это выставит только 1 ничего не делающий метод, в отличие от масштабируемого количества ничего не делающих методов (что, если я захочу поддерживать новое периферийное устройство, такое как VR? Для этого потребуется еще несколько методов, которые можно не использовать). То, что я ничего не делаю, по-прежнему является запахом кода, что предполагает более глубокую проблему дизайна, которую я пытаюсь точно определить. - person Dioxin; 04.02.2017
comment
Давайте продолжим это обсуждение в чате. - person Dioxin; 04.02.2017

Повторяя проблему другу, я считаю, что нашел проблему.

Хотя это позволяет подтипам 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
}

Если кто-то может предложить лучший дизайн, пожалуйста, сделайте это. Этот дизайн был попыткой отделить диспетчеризацию событий от программ, которые хотят использовать пользовательские события, обеспечивая при этом строгую безопасность во время компиляции.

person Dioxin    schedule 04.02.2017