简体   繁体   English

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

[英]How to decouple an event-driven module?

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. 我有一个InputDispatcher ,它将事件(鼠标,键盘等)分派给Game对象。

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. 我想独立于Game扩展InputDispatcherInputDispatcher应该能够支持更多的事件类型,但是不应该强制Game使用它们。

Background 背景

This project uses JSFML. 这个项目使用JSFML。

Input events are handled through the Window class via pollEvents() : List<Event> . 输入事件是通过处理Window类经由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. 我创建了一个GameInputDispatcher类来解决事件处理与处理窗口框架等问题。

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. 在上面的代码( GameInputDispatcher )中,我可以通过创建Game#onEvent(Event)并在默认情况下调用game.onEvent(event)来将事件分派给Game

But that would force Game to write the implementation for sorting & dispatching mouse and keyboard events: 但这会迫使Game编写用于排序和调度鼠标和键盘事件的实现:

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? 如果我想将InputDispacher事件InputDispacherGame ,我怎样才能避免Interface Segregation Principle违规? (by declaring all listening methods: onKeyPressed, onMouseMoved , etc.. inside of Game`, even though they may not be used). (通过声明所有的监听方法: onKeyPressed, onMouseMoved , etc.. inside of Game`中,即使它们可能不被使用)。

Game should be able to choose the form of input it wants to use. Game应该能够选择它想要使用的输入形式。 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. 应通过InputDispatcher扩展支持的输入类型(如鼠标,键,操纵杆等),但不应强制Game支持所有这些输入。

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: 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 subtypes can now implement whatever listener is supported, then register itself: Game子类型现在可以实现支持的任何侦听器,然后注册自己:

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. 虽然这允许Game子类型仅实现他们想要的行为, 但它会强制任何Game声明registerUsing ,即使他们没有实现任何侦听器。

This could be fixed by making registerUsing a default method, having all listeners extend InputListener to redeclare the method: 这可以通过使registerUsing一个default方法来修复,让所有监听器扩展InputListener以重新声明该方法:

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. 但对于我选择创建的每个听众来说,这都是非常繁琐的,违反DRY。

I do not see any point in registerUsing(ListenerRegistrar) . 我在registerUsing(ListenerRegistrar)没有看到任何意义。 If code external to the listener must be written which knows that this is a listener and therefore it needs to register with a ListenerRegistrar , then it may as well go ahead and register the listener with the registrar. 如果必须编写监听器外部的代码,该代码知道这是一个监听器,因此需要向ListenerRegistrar注册,那么它也可以继续向注册ListenerRegistrar注册ListenerRegistrar

The Problem as stated in your question is usually handled in GUIs is by means of default processing , using either inheritance or delegation. 您的问题中所述的问题通常在GUI中处理,是通过默认处理 ,使用继承或委派。

With inheritance, you would have a base class, call it DefaultEventListener or BaseEventListener , whatever you prefer, which has a public void onEvent(Event event) method that contains a switch statement which checks the type of event and invokes an overridable on itself for every event that it knows about. 使用继承,您将拥有一个基类,无论您喜欢什么,都可以将其命名为DefaultEventListenerBaseEventListener ,它具有public void onEvent(Event event)方法,该方法包含一个switch语句,该语句检查事件的类型并为每个事件调用一个可覆盖的事件。它知道的事件。 These overridables generally do nothing. 这些可覆盖的东西通常什么都不做。 Then your "game" derives from this DefaultEventListener and provides overriding implementations only for the events that it cares about. 然后,您的“游戏”派生DefaultEventListener并仅为其关注的事件提供覆盖实现。

With delegation you have a switch statement in which you check for the events that you know about, and in the default clause of your switch you delegate to some defaultEventListener of type DefaultEventListener which probably does nothing. 使用委托,您有一个switch语句,您可以在其中检查您知道的事件,并在您的switchdefault子句中委派给一些DefaultEventListener类型的defaultEventListener ,它可能什么都不做。

There is a variation which combines both: event listeners return true if they process the event, in which case the switch statement immediately returns so that the event will not be processed any further, or false if they do not process the event, in which case the switch statement break s, so code at the end of the switch statement takes control, and what it does is to forward the event to some other listener. 有两种结合的变体:事件监听器在处理事件时返回true ,在这种情况下, switch语句立即返回,以便不再处理事件,如果不处理事件则返回false ,在这种情况下switch语句break ,所以switch语句末尾的代码控制,它的作用是将事件转发给其他侦听器。

An alternative approach (used in many cases in SWT for example) involves registering an observer method for each individual event that you can observe. 另一种方法(例如在SWT中用于许多情况)涉及为您可以观察的每个单独事件注册观察者方法。 If you do this then be sure to remember to deregister every event when your game object dies, or else it will become a zombie. 如果你这样做,那么一定要记得在游戏对象死亡时取消注册每个事件,否则它将成为一个僵尸。 Applications written for SWT are full of memory leaks caused by gui controls that are never garbage-collected because they have some observer registered somewhere, even though they are long closed and forgotten. 为SWT编写的应用程序充满了由gui控件引起的内存泄漏,这些控件从不被垃圾收集,因为它们有一些观察者在某处注册,即使它们被长时间关闭和遗忘。 This is also a source of bugs, because such zombie controls keep receiving events (for example, keyboard events) and keep trying to do things in response, even though they have no gui anymore. 这也是bug的来源,因为这样的僵尸控件不断接收事件(例如,键盘事件)并继续尝试做出响应,即使他们不再有gui。

While reiterating the issue to a friend, I believe I found the issue. 在向朋友重申这个问题的同时,我相信我发现了这个问题。

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. 虽然这允许游戏子类型仅实现他们想要的行为,但它会强制任何游戏声明registerUsing,即使他们没有实现任何侦听器。

This suggests Game is already violating ISP: if clients won't use listeners, Game should not derive from InputListener . 这表明Game已经违反了ISP:如果客户端不使用监听器, Game不应该从InputListener派生。

If, for some reason, a Game subtype did not want to use listeners (maybe interaction is handled via Web pages or the local machine), Game should not be forced to declare registerUsing . 如果由于某种原因, Game子类型不想使用侦听器(可能通过网页或本地机器处理交互),则不应强制Game声明registerUsing

Solution

Instead, an InteractiveGame could derive from Game and implement InputListener : 相反, InteractiveGame可以从Game派生并实现InputListener

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

The framework would then have to check the type of Game to see if it needs to instantiate an InputDispatcher : 然后框架必须检查Game的类型,看它是否需要实例化InputDispatcher

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

If someone can suggest a better design, please do so. 如果有人可以提出更好的设计,请这样做。 This design was an attempt to decouple event dispatching from programs that want to make use of user events, while enforcing strong compile-time-safety. 此设计尝试将事件调度与想要利用用户事件的程序分离,同时强制执行强大的编译时安全性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM