开发者

Java. Correct pattern for implementing listeners

Very typically I have a situation where a given object will need to have many listeners. For instance, I might have

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

but I'll have many such situations. That is, I'll also have a Tiger object that'll have TigerListeners. Now, TigerListeners and ElephantListeners are quite different:

interface TigerListener {
  void listenForGrowl( Growl qrowl );
  void listenForMeow( Meow meow );
}

while

interface ElephantListener {
  void listenForStomp( String location, double intensity );
}

I find that I always have to keep re-implementing the broadcasting mecha开发者_如何学Cnism in each animal class, and the implementation is always the same. Is there a preferred pattern?


Instead of each Listener having specific methods for every event type you can send it, change the interface to accept a generic Event class. You can then subclass Event to specific subtypes if you need, or have it contain state such as double intensity.

TigerListener and ElephentListener then become

interface TigerListener {
    void listen(Event event);
}

In fact, you can then further refactor this interface into a plain Listener:

interface Listener {
    void listen(Event event);
}

Your Listener implementations can then contain the logic that they need for the specific events they care about

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) { 
                ... 
            }
        }
    }
}

The key relationship between the subscriber and the publisher is that the publisher can send events to the subscribers, it isn't necessarily that it can send it certain types of events - this type of refactoring pushes that logic from the interface down into the specific implementations.


This is a more general answer for people who come here just wanting to make a listener. I am summarizing Creating Custom Listeners from CodePath. Read that article if you need more explanation.

Here are the steps.

1. Define an Interface

This is in the child class that needs to communicate with some unknown parent.

public class MyClass {

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

2. Create a Listener Setter

Add a private listener member variable and a public setter method to the child class.

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. Trigger Listener Events

The child object can now call methods on the listener interface. Be sure to check for null because there might not be anyone listening. (That is, the parent class might not have called the setter method for our listener.)

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. Implement the Listener Callbacks in the Parent

The parent can now use the listener that we set up in the child class.

Example 1

public class MyParentClass {

    private void someMethod() {

        MyClass object = new MyClass();
        object.setMyClassListener(new MyClass.MyClassListener() {
            @Override
            public void onSomeEvent(String myString) {
                // handle event
            }
        });
    }
}

Example 2

public class MyParentClass implements MyClass.MyClassListener {

    public MyParentClass() {
        MyClass object = new MyClass();
        object.setMyClassListener(this);
    }

    @Override
    public void onSomeEvent(String myString) {
        // handle event
    }
}


I think you're doing it correct, since your interfaces have semantic value and express what they are listening to (e.g. growls and meows instead of stomps). With a generic approach, you may be able to reuse the broadcasting code, but you may lose the readability.

For example, there is the java.beans.PropertyChangeSupport which is a utility for implementing Oberservers listening for value changes. It does the broadcasting, but you still need to implement the method in your domain class and delegate to the PropertyChangeSupport object. The callback methods are meaningless by themselves, and the events broadcasted are String-based:

public interface PropertyChangeListener extends java.util.EventListener {
     void propertyChange(PropertyChangeEvent evt);
}

Another one is java.util.Observable which provides the broadcasting mechanism, but it's also not the best thing imho.

I like ElephantListener.onStomp()


A different options is the Whiteboard Pattern. This disconnects the publisher and subscriber from each other, and neither will contain any broadcasting code. They both simply use a messaging mechanism for pub/sub and neither has any direct connection to the other.

This is a common model for messaging in an OSGi platform.


I created a Signals library just for this purpose. To remove the boiler code involved in "re-implementing the broadcasting mechanism."

A Signal is an object created automatically from an interface. It has methods for adding listeners and dispatching/broadcasting events.

It looks like this:

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

In this example, Foo2 is the broadcaster of new messages over the Chat interface. Foo then listen to those and log it to logcat.

  • Note that there are no limitations on what interfaces you can use
  • You also have some sugar API for registering for only the first broadcast and unregistering from all the signals at once(Via the SignalsHelper)


Try the java kiss library and you will get this done faster and more correctly.

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

The generator is thread-safe and efficient (writing correct generators is the hardest part). It is an implementation of the ideas in Java Dev. Journal - Skilled Listening in Java (local copy)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜