爪哇。实现监听器的正确模式
Posted
技术标签:
【中文标题】爪哇。实现监听器的正确模式【英文标题】:Java. Correct pattern for implementing listeners 【发布时间】:2011-02-27 21:21:16 【问题描述】:通常情况下,给定对象需要有许多侦听器。例如,我可能有
class Elephant
public void addListener( ElephantListener listener ) ...
但我会遇到很多这样的情况。也就是说,我还将有一个Tiger
对象,该对象将具有TigerListener
s。现在,TigerListener
s 和 ElephantListener
s 完全不同:
interface TigerListener
void listenForGrowl( Growl qrowl );
void listenForMeow( Meow meow );
同时
interface ElephantListener
void listenForStomp( String location, double intensity );
我发现我总是要不断地在每个动物类中重新实现广播机制,而且实现总是一样的。有首选模式吗?
【问题讨论】:
【参考方案1】:我为此创建了一个Signals 库。删除“重新实现广播机制”所涉及的锅炉代码。
信号是从接口自动创建的对象。它具有添加侦听器和调度/广播事件的方法。
看起来像这样:
interface Chat
void onNewMessage(String s);
class Foo
Signal<Chat> chatSignal = Signals.signal(Chat.class);
void bar()
chatSignal.addListener( s-> Log.d("chat", s) ); // logs all the messaged to Logcat
class Foo2
Signal<Chat> chatSignal = Signals.signal(Chat.class);
void bar2()
chatSignal.dispatcher.onNewMessage("Hello from Foo2"); // dispatches "Hello from Foo2" message to all the listeners
在本例中,Foo2
是Chat
接口上的新消息广播者。 Foo
然后收听这些并将其记录到 logcat。
SignalsHelper
)
【讨论】:
【参考方案2】:试试java kiss 库,您会更快、更正确地完成这项工作。
import static kiss.API.*;
class Elephant
void onReceiveStomp(Stomp stomp) ...
class Tiger
void onReceiveMeow(Meow meow) ...
void onReceiveGrowl(Growl growl) ...
class TigerMeowGenerator extends Generator<Meow>
// to add listeners, you get:
// addListener(Object tiger); // anything with onReceiveMeow(Meow m);
// addListener(meow->actions()); // any lambda
// to send meow's to all listeners, use
// send(meow)
生成器是线程安全且高效的(编写正确的生成器是最难的部分)。它是对这些想法的实施 Java Dev. Journal - Skilled Listening in Java (local copy)
【讨论】:
对不起-1,但这是一个关于模式的问题,而不是使用某些库。 链接已失效 - 这是几年前 Java 开发人员杂志上的一篇文章。【参考方案3】:对于来这里只是想成为听众的人来说,这是一个更通用的答案。我正在总结来自 CodePath 的Creating Custom Listeners。如果您需要更多解释,请阅读该文章。
这里是步骤。
1。定义接口
这是在需要与某个未知父级通信的子类中。
public class MyClass
// interface
public interface MyClassListener
// add whatever methods you need here
public void onSomeEvent(String title);
2。创建一个监听器设置器
为子类添加一个私有的监听器成员变量和一个公共的setter方法。
public class MyClass
// add a private listener variable
private MyClassListener mListener = null;
// provide a way for another class to set the listener
public void setMyClassListener(MyClassListener listener)
this.mListener = listener;
// interface from Step 1
public interface MyClassListener
public void onSomeEvent(String title);
3。触发监听事件
子对象现在可以调用侦听器接口上的方法。一定要检查 null 因为可能没有人在听。 (也就是说,父类可能没有为我们的监听器调用 setter 方法。)
public class MyClass
public void someMethod()
// ...
// use the listener in your code to fire some event
if (mListener != null)
mListener.onSomeEvent("hello");
// items from Steps 1 and 2
private MyClassListener mListener = null;
public void setMyClassListener(MyClassListener listener)
this.mListener = listener;
public interface MyClassListener
public void onSomeEvent(String myString);
4。在 Parent 中实现监听器回调
父类现在可以使用我们在子类中设置的监听器了。
示例 1
public class MyParentClass
private void someMethod()
MyClass object = new MyClass();
object.setMyClassListener(new MyClass.MyClassListener()
@Override
public void onSomeEvent(String myString)
// handle event
);
示例 2
public class MyParentClass implements MyClass.MyClassListener
public MyParentClass()
MyClass object = new MyClass();
object.setMyClassListener(this);
@Override
public void onSomeEvent(String myString)
// handle event
【讨论】:
请注意,如果MyClass.setMyClassListener(null)
和MyClass.someMethod()
可以从不同的线程调用,则可能出现NPE。
@rhashimoto,最好的预防方法是什么?
我会使用AtomicReference
成员变量来保存监听器。【参考方案4】:
另一个选项是Whiteboard Pattern。这将发布者和订阅者彼此断开,并且两者都不会包含任何广播代码。他们都只是使用发布/订阅的消息传递机制,并且彼此之间没有任何直接连接。
这是 OSGi 平台中消息传递的常用模型。
【讨论】:
【参考方案5】:我认为您的做法是正确的,因为您的界面具有语义价值并表达了他们所听的内容(例如,咆哮和喵喵叫而不是跺脚)。使用通用方法,您可能可以重用广播代码,但可能会失去可读性。
例如,java.beans.PropertyChangeSupport
是一个用于实现 Oberservers 监听值变化的实用程序。它进行广播,但您仍然需要在域类中实现该方法并委托给 PropertyChangeSupport 对象。回调方法本身没有意义,广播的事件都是基于String的:
public interface PropertyChangeListener extends java.util.EventListener
void propertyChange(PropertyChangeEvent evt);
另一个是java.util.Observable
,它提供了广播机制,但恕我直言,这也不是最好的。
我喜欢ElephantListener.onStomp()
【讨论】:
语义值虽然是一个有效的论点,但会产生紧密耦合(并带来变更风险)。眼光不错,但我不能同意。【参考方案6】:不是每个Listener
都有针对您可以发送的每种事件类型的特定方法,而是更改接口以接受通用Event
类。然后,您可以根据需要将Event
子类化为特定的子类型,或者让它包含诸如double intensity
之类的状态。
TigerListener 和 ElephantListener 然后变成
interface TigerListener
void listen(Event event);
事实上,你可以进一步将该接口重构为一个普通的Listener
:
interface Listener
void listen(Event event);
然后,您的 Listener
实现可以包含他们关心的特定事件所需的逻辑
class TigerListener implements Listener
@Overrides
void listen(Event event)
if (event instanceof GrowlEvent)
//handle growl...
else if (event instance of MeowEvent)
//handle meow
//we don't care about any other types of Events
class ElephentListener
@Overrides
void listen(Event event)
if (event instanceof StompEvent)
StompEvent stomp = (StompEvent) event;
if ("north".equals(stomp.getLocation()) && stomp.getDistance() > 10)
...
订阅者和发布者之间的关键关系是发布者可以向订阅者发送事件,它不一定可以向其发送某些类型的事件——这种类型的重构将接口中的逻辑向下推入具体实现。
【讨论】:
我想我的问题实际上是关于哪种实现更受欢迎。广播机制在我的代码中被重新实现了 3 次(在方案中没有那么多),而您的版本需要一个全新的对象层次结构和 instanceof 语句。有优点也有缺点,但是我该如何根据手头的情况选择正确的方法呢? 另外,当事件类型的数量很大时,关于可读性损失的观点非常有效。 好吧,如果你真的很在意,你可以用泛型替换instanceof
,或者其他一些面向对象的解决方案。我不认为这是一个问题。我将您的问题解释为对代码中侦听器接口的重复定义不满意,这是一种处理方法。而且我真的不明白你关于失去可读性的观点 - 我认为将Event
s 的概念从侦听器接口中分离出来,就不需要重复定义Listener
是什么。
我个人会选择定义一个接口和一个方法来处理(listen(Event)
),而不是重新定义listenFoo()
方法一堆不同的方法。
尽量避免使用泛型进行强制转换。 interface Listener<T> void listen(T event);
以上是关于爪哇。实现监听器的正确模式的主要内容,如果未能解决你的问题,请参考以下文章
Delphi的基于接口(IInterface)的多播监听器模式(观察者模式 ),利用RTTI实现Delphi的多播事件代理研究