爪哇。实现监听器的正确模式

Posted

技术标签:

【中文标题】爪哇。实现监听器的正确模式【英文标题】:Java. Correct pattern for implementing listeners 【发布时间】:2011-02-27 21:21:16 【问题描述】:

通常情况下,给定对象需要有许多侦听器。例如,我可能有

class Elephant 
  public void addListener( ElephantListener listener )  ... 

但我会遇到很多这样的情况。也就是说,我还将有一个Tiger 对象,该对象将具有TigerListeners。现在,TigerListeners 和 ElephantListeners 完全不同:

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
    

在本例中,Foo2Chat 接口上的新消息广播者。 Foo 然后收听这些并将其记录到 logcat。

请注意,您可以使用的接口没有限制 您还有一些糖 API,用于仅注册第一次广播并同时取消注册所有信号(通过 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,或者其他一些面向对象的解决方案。我不认为这是一个问题。我将您的问题解释为对代码中侦听器接口的重复定义不满意,这是一种处理方法。而且我真的不明白你关于失去可读性的观点 - 我认为将Events 的概念从侦听器接口中分离出来,就不需要重复定义Listener 是什么。 我个人会选择定义一个接口和一个方法来处理(listen(Event)),而不是重新定义listenFoo()方法一堆不同的方法。 尽量避免使用泛型进行强制转换。 interface Listener&lt;T&gt; void listen(T event);

以上是关于爪哇。实现监听器的正确模式的主要内容,如果未能解决你的问题,请参考以下文章

Delphi的基于接口(IInterface)的多播监听器模式(观察者模式 ),利用RTTI实现Delphi的多播事件代理研究

今天来个爪哇去边框的小代码

RecyclerView 项目点击监听器的正确方式

Delphi的基于接口(IInterface)的多播监听器模式(观察者模式 )

java中jna使用回调实现事件监听器——观察者模式

揭开观察者设计模式的神秘面纱,手把手教你写监听器