COMP 310
|
Generic Dispatchers and Observers |
![]() ![]() ![]() ![]() ![]() ![]() |
UNDER CONSTRUCTION -- CHECK BACK OFTEN FOR UPDATES!
We would really like to have a little more type safety in our dispatchers since we are now sending a myriad of different messages through them. That is, we would like the dispatcher to be typed to match the message types that its observers are able to process. Otherwise, one could send a message to the observers that they would not understand.
It is important to understand that changing to generic dispatchers and observers has far-reaching implications and will necessitate code changes throughout the BallWorld system.
For a discussion on the issues surrounding the generic typing of dispatchers (Observables) and Observers with respect to eachother, please see the Java Resources page on Covariant and Contravariant Generic Relationships in OO Systems.
This means that at the highest level, our dispatcher interface would look like this:
package util; /** * An dispatcher of messages of type TDispMsg to its registered IObserver<TDispMsg> objects. * The dispatcher is an Observable in the Observer-Observable design pattern * though with the difference of always immediately dispatching to the * observers when a message is received. * * @author Stephen Wong * @author Derek Peirce * * @param <TDispMsg> The type of messages being dispatched */ public interface IDispatcher<TDispMsg> { /** * Dispatch the given message to all the registered Observers * @param msg The message to pass to all the observers */ public void dispatch(TDispMsg msg); /** * Register the given observer in the dispatcher * @param obs The observer to register */ public void addObserver(IObserver<TDispMsg> obs); /** * Deregister the given observer from this dispatcher. * @param obs The observer to deregister */ public void deleteObserver(IObserver<TDispMsg> obs); /** * Deregister all observers from this dispatcher. */ public void deleteObservers(); }
Note: The name of the dispatching method above has been changed from
notifyAll()
to
dispatch()
to avoid conflicts with the inherited Object.notifyAll()
which is used for thread management.
The observer is thus similarly defined:
package util; /** * An observer for IDispatcher<TDispMsg>. When registered with a dispatcher, * an observer will receive the TDispMsg-type message that the dispatcher is sends * to its registered observers. * * @author Stephen Wong * @author Derek Peirce * * @param <TDispMsg> The type of message that this observer can process */ public interface IObserver<TDispMsg> { /** * The method that the dispatcher will call * to process the supplied message. * @param dispatcher The dispatcher sending the message * @param msg The message for this observer to process. */ public void update(IDispatcher<TDispMsg> dispatcher, TDispMsg msg); }
Since our observers are Ball
objects that understand
IBallCmd
messages, the Ball
class must implement
IObserver<IBallCmd>
:
public class Ball implements IObserver<IBallCmd> { ... }
IBallCmds
must also be typed to the dispatcher that is sending them to the
Balls
, which necessarily is a dispatcher that sends IBallCmds
:
package ballworld.model; import util.IDispatcher; /** * Interface that represents commands sent through the dispatcher to process the balls * */ @FunctionalInterface public abstract interface IBallCmd { /** * The method run by the ball's update method which is called when the ball is updated by the dispatcher. * @param context The ball that is calling this method. The context under which the command is to be run. */ public abstract void apply(Ball context, IDispatcher<IBallCmd> dispatcher); }
Note: By annotating IBallCmd
as a
FunctionalInterface
, this enables us to have full compiler support when
implementing IBallCmds
using lambda expresssions.
Generally, in the BallWorld
system, TDispMsg
will be set to be IBallCmd
since that is the type of message that the Ball
objects are capable
of processing.
Our dispatcher-observer system is now fully type-safe:
// In the model: private IDispatcher<IBallCmd> _dispatcher = new SetDispatcherSequential<IBallCmd>(); // or any other implementation ... // code where the dispatcher is invoked... _dispatcher.dispatch(new IBallCmd() { @Override /** * Tells the given ball to updateState, move and bounce. */ public void apply(Ball context, IDispatcher<IBallCmd> disp) { context.updateState(disp); context.move(); context.bounce(); } }); // Or in terms of lambda expressions: _dispatcher.dispatch((context, disp) -> { context.updateState(disp); context.move(); context.bounce(); }); // ----------------------- // In the ball: public void update(IDispatcher<IBallCmd> o, IBallCmd cmd) { cmd.apply(this, o); }
And since the IUpdateStrategies
are capable of sending their own
messages out the dispatcher, the IUpdateStrategies
must be typed to
the kind of messages that the supplied IDispatcher
can send out:
package ballworld.model; import util.IDispatcher; /** * The strategy that runs when a Ball updates its state. * * @author Stephen Wong * * @param TDispMsg The type of message that the supplied IDispatcher can send. */ public interface IUpdateStrategy<TDispMsg> { /** * Initializes the strategy. Should be called every time the Ball sets a new strategy. * @param context The ball using this strategy. */ public void init(Ball context); /** * Update the state of the context Ball. * @param context The context (host) Ball whose state is to be updated * @param dispatcher The Dispatcher who sent the command that is calling through to here. */ public void updateState(Ball context, IDispatcher<TDispMsg> dispatcher); /** * A factory for a typed null strategy object.
* Usage: instantiate this factory class using the desired TDispMsg type and then call its make() method * to create the correctly typed null strategy object. */ public static final class NullFactory<TDispMsg> implements IUpdateStrategyFac<TDispMsg> { /** * Returns a no-op null strategy */ @Override public IUpdateStrategy<TDispMsg> make() { return new IUpdateStrategy<TDispMsg>() { @Override /** * No-op * @param context Ignored */ public void init(Ball context) { } @Override /** * No-op * @param context Ignored * @param dispatcher Ignored */ public void updateState(Ball context, IDispatcher<TDispMsg> dispatcher) { } }; } } }
Notes:
init()
method has been added to enable the strategy to
be initialized to its host Ball
when it is loaded into its
host. This necessitates changes to the Ball
's
constructor and setStrategy()
methods to be sure that this init()
method is called.
IMPORTANT: The above changes are NOT the only changes that are required in the BallWorld system to change over to a generic dispatcher and observers! Most of the model code will require changes which for the most part will be relatively minor additions of generic type specifications. The controller code will thus also be affected. The view code, being decoupled from the model, should not be affected!
When adding in generic type specifications, always
try to type your classes and methods to be as minimally restricted as possible.
If the class does not explicitly depend on the message type being an
IBallCmd
, then leave the class with an unspecified generic type, i.e.
TDispMsg
. For instance, IUpdateStrategies
such as StraightStrategy
, CurveStrategy
,
SwitcherStrategy
and MultiStrategy
do not care what kind of
message the dispatcher sends, so they should be left with unspecified
TDispMsg
generic typing.
IDispatcher
ImplementationThe following is an example of an implementation hieirarchy for IDispatcher
that uses a thread-safe Set
for internal storage of the
IObservers
.
Two concrete implementations are shown, one that dispatches to the observers
sequentially and one that dispatches in parallel:
package util; import java.util.Collection; /** * An abstract Collection-based IDispatcher. * * @author Stephen Wong * @author Derek Peirce * * @param <TDispMsg> The type of message sent to the registered IObservers */ public abstract class ACollectionDispatcher<TDispMsg> implements IDispatcher<TDispMsg> { /** * Stores the observers. */ private final Collection<IObserver<TDispMsg>> observers; /** * Constructor for the class. The Collection that is used needs to be supplied, * generally by the implementing subclass. This allows for different types of * Collections to be used for different purposes. It is highly recommended that the * supplied Collection be completely thread-safe to enable the use of * multiple dispatching threads. * @param observers The Collection of IObserver<TDispMsg> to use. */ public ACollectionDispatcher(Collection<IObserver<TDispMsg>> observers) { this.observers = observers; } /** * Accessor method for the internal Collection for use by implementing subclasses. * @return The internal Collection of IObservers<TDispMsg> */ protected Collection<IObserver<TDispMsg>> getCollection() { return observers; } /** * {@inheritDoc}<br/> * Implementation: Add the given observer to the internal Collection. */ @Override public void addObserver(IObserver<TDispMsg> obs) { observers.add(obs); } /** * {@inheritDoc}<br/> * Implementation: Delete the given observer from the internal Collection. */ @Override public void deleteObserver(IObserver<TDispMsg> obs) { observers.remove(obs); } /** * {@inheritDoc}<br/> * Implementation: Delete all the observers from the internal Collection. */ @Override public void deleteObservers() { observers.clear(); } }
Note: The Javadoc directive, {inheritDoc}
, will insert the
documentation from the overridden superclass method at that location. The
<br/>
is added to put the statement(s) about the specific
implementation on the next line of the documenation and thus visually separate them
from the inherited superclass documentation of the method.
package util; import java.util.concurrent.CopyOnWriteArraySet; /** * A Collection-based Dispatcher that uses a CopyOnWriteArraySet. * * @author Stephen Wong * @author Derek Peirce * * @param <TDispMsg> The type of message sent to the registered IObservers */ public abstract class ASetDispatcher<TDispMsg> extends ACollectionDispatcher<TDispMsg> { /** * The constructor for the class that supplies a CopyOnWriteArraySet instance to the superclass constructor. */ public ASetDispatcher() { super(new CopyOnWriteArraySet<>()); // Type of CopyOnWriteArraySet is inferred by compiler } }
package util; /** * A CopyOnWriteArraySet-based IDispatcher that dispatches to its IObservers sequentially. * * @author Stephen Wong * @author Derek Peirce * * @param <TDispMsg> The type of message sent to the registered IObservers */ public class SetDispatcherSequential<TDispMsg> extends ASetDispatcher<TDispMsg> { /** * {@inheritDoc}<br/> * Implementation: Sequential iteration through the collection of IObservers. */ @Override public void dispatch(TDispMsg msg) { getCollection().forEach(o -> { o.update(this, msg); }); } }
package util; /** * A CopyOnWriteArraySet-based IDispatcher that dispatches to its IObservers in parallel. * * @author Stephen Wong * @author Derek Peirce * * @param <TDispMsg> The type of message sent to the registered IObservers */ public class SetDispatcherParallel<TDispMsg> extends ASetDispatcher<TDispMsg> { /** * {@inheritDoc}<br/> * Implementation: Attempts to perform parallel dispatching of the message to the collection of IObservers. * Note that parallel execution is not guaranteed. */ @Override public void dispatch(TDispMsg msg) { getCollection().parallelStream().forEach(o -> { o.update(this, msg); }); } }
The parallel-dispatching dispatcher has the potential to cause markedly different and/or incorrect and/or deadlocking behavior in the BallWorld system. Try it and see what happens -- if things change, try to figure out why!
© 2016 by Stephen Wong