Java. 实现监听器的正确模式

29

非常典型的情况下,一个特定的对象需要有很多监听器。例如,我可能会有:

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 );
}

我发现我总是需要在每个动物类中重新实现广播机制,而且实现方式总是相同的。有没有一种推荐的模式?

6个回答

31

不要为每种事件类型为每个Listener提供特定的方法,而是更改接口以接受通用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) { 
                ... 
            }
        }
    }
}

订阅者和发布者之间的关键关系在于发布者可以向订阅者发送事件,而不一定是它可以发送某些特定类型的事件 - 这种重构将该逻辑从接口推到具体实现中。


我想我的问题实际上是关于哪种实现方式更受欢迎。在我的代码中,广播机制被重新实现了三次(在事物的范围内并不算太多),而你的版本则需要一个全新的对象层次结构和instanceof语句。这种方法有优点和缺点,但我该如何选择适合手头情况的正确方法呢? - Jake
此外,当事件类型数量较大时,可读性的丧失是一个非常合理的观点。 - Jake
如果你真的关心这个问题,你可以用泛型替换instanceof,或者其他面向对象的解决方案。我不认为这是一个问题。我理解你的问题是不满意在代码中重复定义监听器接口,这是一种处理方法。而且我并不认为你失去了可读性——我认为将“事件”概念与监听器接口分离,消除了重复定义“监听器”的需要。 - matt b
1
个人而言,我会选择定义一个接口和一个方法来处理(listen(Event)),而不是以多种方式重新定义listenFoo()方法。 - matt b
尽量避免使用泛型进行强制转换。interface Listener<T>{ void listen(T event); } - darkconeja
请阅读有关SOLID原则的内容,并避免使用每个新类都要支持的instanceof查询列表。 - Denis Reichelt

25

对于只想创建一个监听器的人来说,这是一个更通用的答案。我正在总结CodePath的创建自定义监听器。如果您需要更多解释,请阅读该文章。

以下是步骤。

1. 定义一个接口

这是需要与某个未知父级进行通信的子类中的步骤。

public class MyClass {

    // interface
    public interface MyClassListener {
        // add whatever methods you need here
        public void onSomeEvent(String title);
    }
}

2. 创建一个监听器设置器

在子类中添加一个私有的监听器成员变量和一个公共的设置器方法。

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,因为可能没有任何人在监听(也就是说,父类可能没有调用我们的监听器的设置方法)。

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. 在父类中实现监听器回调

现在,父类可以使用我们在子类中设置的监听器。

示例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
@rhashimoto,最好的预防方法是什么? - Suragch
1
我会使用一个AtomicReference成员变量来保存监听器。 - rhashimoto

3
我认为你的方法是正确的,因为你的接口具有语义价值并表达了它们正在监听的内容(例如 growls 和 meows 而不是 stomps)。采用通用方法,你可能能够重用广播代码,但可能会失去可读性。
例如,有一个 java.beans.PropertyChangeSupport 工具,用于实现监听值更改的观察者。它进行广播,但你仍然需要在你的领域类中实现该方法并委托给 PropertyChangeSupport 对象。回调方法本身没有意义,并且广播的事件基于字符串。
public interface PropertyChangeListener extends java.util.EventListener {
     void propertyChange(PropertyChangeEvent evt);
}

另一个是java.util.Observable,它提供了广播机制,但在我看来并不是最好的选择。我喜欢ElephantListener.onStomp()

语义值虽然是一个有效的参数,但会导致紧密耦合(和变更风险)。很有眼光,但我不能同意。 - Justin

2
我为此创建了一个 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。
  • 请注意,您可以使用任何接口。
  • 您还可以使用注册仅第一次广播和一次取消所有信号的糖果 API(通过 SignalsHelper)。

2
另一种选择是白板模式。这将发布者和订阅者彼此分离,它们都不包含任何广播代码。它们只是使用消息机制进行发布/订阅,而彼此之间没有直接连接。
在OSGi平台中,这是一种常见的消息传递模型。

-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)中思想的一种实现。

2
抱歉得分为-1,但这是关于模式的问题,而不是使用某些库的问题。 - Kamil
链接已失效 - 这是多年前《Java开发者杂志》上的一篇文章。 - Warren MacEvoy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接