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 TigerListener
s. Now, TigerListener
s and ElephantListener
s 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)
精彩评论