As a subscriber, the Java code for adding a listener is not bad: just call the publisher's
addListener
method,
typically passing an anonymous inner class that defines the
callback method.
But on the publisher side, you have to implement
addListener
, removeListener
and fireEvent
methods for the published event.
In Scala, all of that publisher boilerplate to manage listeners can be replaced by three words. Sweet! And the inner class boilerplate in the subscriber disappears also.
I implemented a Scala trait called
ListenerManager
to be used by a publisher.
This trait defines the methods to add and remove listeners,
and to fire off the published events:
To add the ability to a class to publish an event, just addtrait ListenerManager[E] { type L = (E) => Unit private var listeners: List[L] = Nil private var listenersLock = new Object() /** True if the listener is already in our list. */ private def isListener(listener:L) = listeners.exists(_==listener) /** Add a listener to our list if it is not already there. */ def addListener(listener:L) = listenersLock.synchronized { if (!isListener(listener)) listeners = listener :: listeners } /** Remove a listener from our list. If not in the list, ignored. */ def removeListener(listener:L):Unit = listenersLock.synchronized { listeners = listeners.filter(_!=listener) } /** Apply the given closure to all of the listeners in the list. */ protected def fireEvent(event:E) = listenersLock.synchronized { listeners.foreach(_.apply(event)) } }
with ListenerManager[MyEvent]
(these are the three words
mentioned above) to the defining
class
line for that class.
This will define the addListener
and removeListener
methods for subscribers to call,
and the fireEvent
method for the publisher to call.
Let's take a look at the above code in more detail.
The type
[E]
in the ListenerManager
trait declaration
is the type of the event to be published.
The publisher class would typically define a one-line case class
for that event type that lists the data of interest.
For example:
The first line of the body ofcase class ButtonEvent(source:Button, action:Integer)
ListenerManager
defines the
type of the listeners we are managing,
which is a function that takes an instance of our event and does not
return a value.
In the subscriber, the code to add a listener passes in a function
of this type, which can be specified in-line as a closure:
The call topublisher.addListener((x:ButtonEvent)=>(println("Button info:"+x)))
println
could be replaced by a call to a
local method that does more complicated processing of the event.
Next we define a list of listeners and the add and remove methods to manipulate that list. Our private
isListener
method, used to ensure that
a listener does not get into the list more than once, is only used
in one place so could have been done in-line, but has been written
separately to add a bit of clarity to the code.
Last is the
fireEvent
method that publishes the event.
For each listener in our list, it calls that listener's
apply
method, which is how to invoke a function when
given it as an object.
The
addListener
, removeListener
and
fireEvent
methods are synchronized in case the application
is using separate threads for the publisher and each subscriber.
We can't lock on the listeners
list because we
recreate that each time a listener is added and removed,
and to minimize locking contention we would rather not lock on the
publishing object, so we add another object
(listenersLock
) just for locking the methods
that access the listener list, and we synchronize on that object.
Synchronizing the
fireEvent
method means that any
subscriber calling addListener
or removeListener
will block until all of the callbacks are done.
We could choose not to synchronize fireEvent
to prevent
that blocking,
and that method will still work
because once it picks up the listeners list and starts iterating through
it, it will continue to use that list even if someone else calls
addListener
or removeListener
.
However, doing so would
mean it would be possible for a listener to be called
after the subscriber has called removeListener
,
which is probably more of an issue than blocking when
adding or removing a listener while callbacks are happening.
We will assume that listeners will follow the standard guideline
that they should keep processing during the callback to a minimum,
and that it they need to do a lot of work based on a callback,
they will implement a mechanism to do that work in another thread
so as not to use up a lot of time on the publisher's thread.
For the subscriber, using
ListenerManager
saves the boilerplate
of an inner class.
For the publisher, using ListenerManager
saves a lot more,
making it possible to set up
publication just by defining a case class for the published event,
adding the with
declaration to the class definition, and
adding calls to the fireEvent
method at publication points.
Pretty easy.
I could have implemented the
ListenerManager
trait to take
an instance of
a callback class with an action
method, as is done in Java.
Or, to take more of a Scala approach, I could have switched from
using the
Listener pattern as is used in Java to an
Actor-based Publish/Subscribe implementation.
For this particular case I chose a simple path down the middle,
keeping the basic Java Listener concept but eliminating the boilerplate.
1 comment:
I already have plans for using this-great post Jim!
Post a Comment