可能需要一些背景知识,但如果您有信心,请跳至问题。希望摘要能够解决问题。
我有InputDispatcher
将事件(鼠标,键盘等)分派给Game
对象。
我想独立于InputDispatcher
扩展Game
: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
并在默认情况下调用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。
答案 0 :(得分:1)
我在registerUsing(ListenerRegistrar)
中没有看到任何意义。如果必须写入监听器外部的代码,知道这是一个监听器,因此需要使用ListenerRegistrar
进行注册,那么它也可以继续向注册器注册监听器。
您的问题中陈述的问题通常在GUI中处理,是通过默认处理,使用继承或委派。
使用继承,您将拥有一个基类,称之为DefaultEventListener
或BaseEventListener
,无论您喜欢什么,它都有一个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
}
如果有人可以推荐更好的设计,请这样做。此设计尝试将事件调度与想要利用用户事件的程序分离,同时强制执行强大的编译时安全性。