【问题标题】: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 );
}

我发现我总是要不断地在每个动物类中重新实现广播机制,而且实现总是一样的。有首选模式吗?

【问题讨论】:

    标签: java design-patterns listeners


    【解决方案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); }
        猜你喜欢
        • 1970-01-01
        • 2014-09-07
        • 1970-01-01
        • 1970-01-01
        • 2019-06-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多