3

可能需要一点背景知识,但如果您有信心,请跳至问题。希望总结能说明问题。

概括

我有一个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来分派事件。GameGame#onEvent(Event)game.onEvent(event)

但这将强制Game编写用于排序和调度鼠标和键盘事件的实现:

class DemoGame implements Game {
    public void onEvent(Event event) {
        // what kind of event?
    }
}

问题

如果我想从InputDispacherinto提供事件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。

4

2 回答 2

1

我看不出有什么意义registerUsing(ListenerRegistrar)。如果必须编写侦听器外部的代码,知道这是一个侦听器,因此它需要向 a 注册ListenerRegistrar,那么它不妨继续向注册器注册侦听器。

您的问题中所述的问题通常在 GUI 中通过默认处理方式处理,使用继承或委托。

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

使用委托,您有一个switch语句,您可以在其中检查您知道的事件,并在您的default子句中您switch委托给一些可能什么都不做defaultEventListener的类型。DefaultEventListener

有一种结合了两者的变体:如果事件侦听器true处理了事件,则返回,在这种情况下,switch语句立即返回,这样事件就不会被进一步处理,或者false如果它们不处理事件,在这种情况下,switch语句break,因此语句末尾的代码获得switch控制权,它所做的是将事件转发给其他侦听器。

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

于 2017-02-03T23:06:28.533 回答
0

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

尽管这允许 Game 子类型仅实现它们想要的行为,但它强制任何 Game 声明 registerUsing,即使它们没有实现任何侦听器。

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

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

解决方案

相反, anInteractiveGame可以派生自Game并实现InputListener

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

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

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

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

于 2017-02-04T21:39:11.817 回答