如何解耦事件驱动的模块?

时间:2017-02-03 22:48:27

标签: java decoupling event-driven jsfml

可能需要一些背景知识,但如果您有信心,请跳至问题。希望摘要能够解决问题。

摘要

我有InputDispatcher将事件(鼠标,键盘等)分派给Game对象。

我想独立于InputDispatcher扩展GameInputDispatcher应该能够支持更多事件类型,但不应强制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 个答案:

答案 0 :(得分:1)

我在registerUsing(ListenerRegistrar)中没有看到任何意义。如果必须写入监听器外部的代码,知道这是一个监听器,因此需要使用ListenerRegistrar进行注册,那么它也可以继续向注册器注册监听器。

您的问题中陈述的问题通常在GUI中处理,是通过默认处理,使用继承或委派。

使用继承,您将拥有一个基类,称之为DefaultEventListenerBaseEventListener,无论您喜欢什么,它都有一个public void onEvent(Event event)方法,其中包含一个switch语句,用于检查类型event并为它知道的每个事件调用一个overridable。这些可覆盖的东西通常什么都不做。然后,您的“游戏”派生自此DefaultEventListener,并仅为其关注的事件提供覆盖实现。

通过委派,您可以使用switch语句检查您所了解的事件,并在default的{​​{1}}子句中委托您switch defaultEventListener类型可能什么都不做。

有一个变体将两者结合起来:如果事件监听器处理事件,则返回DefaultEventListener,在这种情况下,true语句会立即返回,以便不再处理事件,或{ {1}}如果他们不处理事件,在这种情况下switch语句false,那么switch语句末尾的代码将获得控制权,它的作用是将事件转发给其他听众。

另一种方法(例如在SWT中用于许多情况)涉及为您可以观察到的每个单独事件注册观察者方法。如果你这样做,那么一定要记得在游戏对象死亡时取消注册每个事件,否则它将成为一个僵尸。为SWT编写的应用程序充满了由gui控件引起的内存泄漏,这些控件从不被垃圾收集,因为它们有一些观察者在某处注册,即使它们被长时间关闭和遗忘。这也是一个bug的来源,因为这样的僵尸控件不断接收事件(例如,键盘事件)并继续尝试做出响应,即使他们已经没有gui了。

答案 1 :(得分:0)

在向朋友重申这个问题的同时,我相信我发现了这个问题。

  

虽然这允许Game子类型只实现他们想要的行为,但它会强制任何Game声明registerUsing,即使他们没有实现任何监听器。

这表明Game已经违反了ISP:如果客户端不使用侦听器,则Game不应来自InputListener

如果出于某种原因,Game子类型不想使用侦听器(可能通过Web页面或本地计算机处理交互),则不应强制Game声明registerUsing }}

解决方案

相反,InteractiveGame可以来自Game并实施InputListener

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

然后框架必须检查Game的类型,看它是否需要实例化InputDispatcher

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

如果有人可以推荐更好的设计,请这样做。此设计尝试将事件调度与想要利用用户事件的程序分离,同时强制执行强大的编译时安全性。