簡體   English   中英

如何解耦事件驅動的模塊?

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

可能需要一點背景,但如果您有信心則跳到問題 希望摘要能夠解決問題。

摘要

我有一個InputDispatcher ,它將事件(鼠標,鍵盤等)分派給Game對象。

我想獨立於Game擴展InputDispatcherInputDispatcher應該能夠支持更多的事件類型,但是不應該強制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#onEvent(Event)並在默認情況下調用game.onEvent(event)來將事件分派給Game

但這會迫使Game編寫用於排序和調度鼠標和鍵盤事件的實現:

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

如果我想將InputDispacher事件InputDispacherGame ,我怎樣才能避免Interface Segregation Principle違規? (通過聲明所有的監聽方法: 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。

我在registerUsing(ListenerRegistrar)沒有看到任何意義。 如果必須編寫監聽器外部的代碼,該代碼知道這是一個監聽器,因此需要向ListenerRegistrar注冊,那么它也可以繼續向注冊ListenerRegistrar注冊ListenerRegistrar

您的問題中所述的問題通常在GUI中處理,是通過默認處理 ,使用繼承或委派。

使用繼承,您將擁有一個基類,無論您喜歡什么,都可以將其命名為DefaultEventListenerBaseEventListener ,它具有public void onEvent(Event event)方法,該方法包含一個switch語句,該語句檢查事件的類型並為每個事件調用一個可覆蓋的事件。它知道的事件。 這些可覆蓋的東西通常什么都不做。 然后,您的“游戲”派生DefaultEventListener並僅為其關注的事件提供覆蓋實現。

使用委托,您有一個switch語句,您可以在其中檢查您知道的事件,並在您的switchdefault子句中委派給一些DefaultEventListener類型的defaultEventListener ,它可能什么都不做。

有兩種結合的變體:事件監聽器在處理事件時返回true ,在這種情況下, switch語句立即返回,以便不再處理事件,如果不處理事件則返回false ,在這種情況下switch語句break ,所以switch語句末尾的代碼控制,它的作用是將事件轉發給其他偵聽器。

另一種方法(例如在SWT中用於許多情況)涉及為您可以觀察的每個單獨事件注冊觀察者方法。 如果你這樣做,那么一定要記得在游戲對象死亡時取消注冊每個事件,否則它將成為一個僵屍。 為SWT編寫的應用程序充滿了由gui控件引起的內存泄漏,這些控件從不被垃圾收集,因為它們有一些觀察者在某處注冊,即使它們被長時間關閉和遺忘。 這也是bug的來源,因為這樣的僵屍控件不斷接收事件(例如,鍵盤事件)並繼續嘗試做出響應,即使他們不再有gui。

在向朋友重申這個問題的同時,我相信我發現了這個問題。

雖然這允許游戲子類型僅實現他們想要的行為,但它會強制任何游戲聲明registerUsing,即使他們沒有實現任何偵聽器。

這表明Game已經違反了ISP:如果客戶端不使用監聽器, 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
}

如果有人可以提出更好的設計,請這樣做。 此設計嘗試將事件調度與想要利用用戶事件的程序分離,同時強制執行強大的編譯時安全性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM