[英]How to decouple an event-driven module?
可能需要一點背景,但如果您有信心則跳到問題 。 希望摘要能夠解決問題。
我有一個InputDispatcher
,它將事件(鼠標,鍵盤等)分派給Game
對象。
我想獨立於Game
擴展InputDispatcher
: 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#onEvent(Event)
並在默認情況下調用game.onEvent(event)
來將事件分派給Game
。
但這會迫使Game
編寫用於排序和調度鼠標和鍵盤事件的實現:
class DemoGame implements Game {
public void onEvent(Event event) {
// what kind of event?
}
}
如果我想將InputDispacher
事件InputDispacher
給Game
,我怎樣才能避免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中處理,是通過默認處理 ,使用繼承或委派。
使用繼承,您將擁有一個基類,無論您喜歡什么,都可以將其命名為DefaultEventListener
或BaseEventListener
,它具有public void onEvent(Event event)
方法,該方法包含一個switch語句,該語句檢查事件的類型並為每個事件調用一個可覆蓋的事件。它知道的事件。 這些可覆蓋的東西通常什么都不做。 然后,您的“游戲”派生DefaultEventListener
並僅為其關注的事件提供覆蓋實現。
使用委托,您有一個switch
語句,您可以在其中檢查您知道的事件,並在您的switch
的default
子句中委派給一些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.