COMP 310
Spring 2019

Generic Dispatchers and Observers

Home  Info  Canvas   Java Resources  Eclipse Resources  Piazza

Since dispatchers are capable of sending a myriad of different messages, it is important that 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.

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 provided.util.dispatcher;

import java.util.Set;

/**
 * Top-level abstraction of a Dispatcher which is the Observable in the 
 * Observer-Observable Design Pattern without the requirement to set the state 
 * before sending a message to the observers.
 * 
 * A Dispatcher sends IMsg-type messages to all its observers which therefore must 
 * be capable of receiving and processing those messages, i.e. are IObserver<TMSg>.
 *
 * To maximally decouple an application from the randomizer's implementation any variable representing a randomizer
 * should be typed to this interface, not to any concrete implementation.  For example:
 * IDispatcher<Graphics> myDispatcher = new SequentialDispatcher<Graphics>() 
 * 
 * @author swong
 *
 * @param <TMsg>   The type of message to send to all the observers.
 */
public interface IDispatcher<TMsg> {

	/**
	 * Add an observer to the dispatcher.   If the observer is already in the dispatcher, 
	 * as determined by the comparison (equals()) process, the dispatcher is NOT mutated and false is returned. 
	 * @param obs  The IObserver to add
	 * @return true if the given observer was not already in the dispatcher, false otherwise.
	 * @throws ClassCastException If the observer cannot be properly compared against the existing observers
	 * @throws NullPointerException If the supplied value is null
	 */
	public boolean addObserver(IObserver<TMsg> obs);

	/**
	 * Remove an observer from the dispatcher.   The dispatcher does not make any 
	 * assumptions that the observer being removed is identically the same object as 
	 * that it was requested to remove via the input parameter.  The returned object 
	 * is the object that was internally held by the dispatcher.
	 * @param obs  The IObserver to add
	 * @return The observer that was removed or null if it was not found.
	 */
	public IObserver<TMsg> removeObserver(IObserver<TMsg> obs);

	/**
	 * Get a COPY of the set of all the observers currently in the dispatcher.   This is a 
	 * shallow copy, so the observers themselves are not copied.
	 * @return A set of IObservers
	 */
	public Set<IObserver<TMsg>> getAllObservers();

	/**
	 * Removes all the observers currently in the dispatcher
	 * @return A COPY of the set of IObservers in the dispatcher before they were all removed.
	 */
	public Set<IObserver<TMsg>> removeAllObservers();

	/**
	 * Send the given message to all the observers in the dispatcher 
	 * @param msg   The IMsg to send to all the observers
	 */
	public void updateAll(TMsg msg);
}

 

The observer is thus similarly defined:

package provided.util.dispatcher;

/**
 * Top-level abstraction of the Observer in the Observer-Observable Design Pattern.
 * An IObserver receives TMsg-type messages from its associated, matched Observable, 
 * an IDispatcher<TMsg>. 
 * @author swong
 *
 * @param <TMsg>  The type of message this observer is capable of receiving and processing.
 */
public interface IObserver<TMsg> {

	/**
	 * The update method called by the observer's dispatcher (observable)
	 * @param disp  The dispatcher that called this method
	 * @param msg The message being distributed to all the observers.
	 */
	public void update(IDispatcher<TMsg> disp, TMsg 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 provided.util.dispatcher.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, TMsg 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.updateAll(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. But since the strategies can only be used in the context of a Ball, which is defined as only accepting IBallCmd messages, then the srategies need only be typed in terms of those types of messages:

package ballworld.model;

import provided.util.dispatcher.IDispatcher;

/**
 * The strategy that runs when a Ball updates its state.
 * 
 * @author Stephen Wong
 * 
 */
public interface IUpdateStrategy {

	/**
	 * 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<IBallCmd> dispatcher);
	
	/**
	 * Null strategy
	 */
	public static final IUpdateStrategy NULL =  new IUpdateStrategy() {

				@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<IBallCmd> dispatcher) {
				}		
			};



}

Notes:

 

IMPORTANT:   Be sure that the Ballworld model uses properly typed generics and not "raw" types. Using raw types negates all of the type-safety advantages of the generic dispatchers and other generics in the system.   Note that the view code, being decoupled from the model, should not be affected by any generics in the model!  

 

Example IDispatcher Implementation

The 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.

First, let's define an intermediate abstract class that implements IDispatcher using an internal collection of IObservers. Here. a ConcurrentSkipList is used because it offers thread-safety for multi-threaded implementations plus has the ability to easily delete elements.

package provided.util.dispatcher.impl;

import java.util.Comparator;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;

import provided.util.dispatcher.IDispatcher;
import provided.util.dispatcher.IObserver;

/**
 * Mid-level abstraction of a dispatcher that establishes the ability to hold and manage
 * observers in a thread-safe manner.
 * @author swong
 *
 * @param <TMsg>  The type of messages that will be sent out to the observers.
 */
public abstract class ADispatcher<TMsg> implements IDispatcher<TMsg> {

	/**
	 * Comparator used to order the IObservers in the ConcurrentSkipListSet.   This is needed
	 * because IObservers are not naturally Comparable.   Returns zero if the two IObservables are 
	 * equal, otherwise returns -1 if the first object's hashcode is less than or equal to the second object's
	 * hashcode or +1 if the first object's hashcode is greater than the second object's hashcode. 
	 */
	private Comparator<IObserver<TMsg>> comparator = new Comparator<IObserver<TMsg>>() {
		@Override
		public int compare(IObserver<TMsg> o1, IObserver<TMsg> o2) {
			if (o1.equals(o2)) {
				return 0;
			} else {
				if (o1.hashCode() <= o2.hashCode()) {
					return -1;
				} else {
					return +1;
				}
			}
		}

	};

	/**
	 * The internal data storage of observers.  Needs to be thread-safe.  
	 * For systems that have few mutations of the set, a CopyOnWriteArraySet 
	 * could be used for better read performance and smaller data size.
	 */
	private ConcurrentSkipListSet<IObserver<TMsg>> observers = new ConcurrentSkipListSet<IObserver<TMsg>>(comparator);
	//	private CopyOnWriteArraySet<IObserver<TMsg>> observers = new CopyOnWriteArraySet<IObserver<TMsg>>();

	/**
	 * Protected method to allow implementing subclasses access to the set of observers.
	 * The actual set of observers is returned and is thus mutable.
	 * @return The set of observers currently in use.  This is NOT a copy.
	 */
	protected Set<IObserver<TMsg>> getObserverSet() {
		return observers;
	}

	@Override
	public boolean addObserver(IObserver<TMsg> obs) {
		return observers.add(obs);
	}

	/**
	 * {@inheritDoc}<br>
	 * Implementation: Returns the actual object in the internal collection that is equal to the given object.
	 */
	@Override
	public IObserver<TMsg> removeObserver(IObserver<TMsg> obs) {
		IObserver<TMsg> foundObs = null; // in case obs is not in the collection.
		// Note that equality does not guarantee that two objects are identically the same entity, 
		// so must retrieve the actual entity in the collection.
		if (observers.contains(obs)) { // make sure it is in the collection 
			foundObs = observers.floor(obs); // returns the desired obs only because we already know that it is in the collection.
			observers.remove(obs); // remove it from the collection
		}
		return foundObs;

	}

	/**
	 * {@inheritDoc}<br>
	 * Implementation: Returns a copy by cloning the internal collection.
	 */
	@Override
	public Set<IObserver<TMsg>> getAllObservers() {
		return observers.clone();
		//		return new CopyOnWriteArraySet<IObserver<TMsg>>(observers);
	}

	/**
	 * {@inheritDoc}<br>
	 * Implementation: Returns a clone of the original internal collection from before it was cleared.
	 */
	@Override
	public Set<IObserver<TMsg>> removeAllObservers() {
		Set<IObserver<TMsg>> original_set = this.getAllObservers();
		observers.clear();
		return original_set;
	}

}

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.

A sequential dispatcher simply sends the given message to each observer, one-by-one on a single thread:

 
package provided.util.dispatcher.impl;

/**
 * A simple dispatcher that updates its observers sequentially on a single thread.
 * @author swong
 *
 * @param <TMsg>  The type of message being sent to the observers.
 */
public class SequentialDispatcher<TMsg> extends ADispatcher<TMsg> {
	@Override
	public void updateAll(TMsg msg) {
		this.getObserverSet().forEach((obs) -> obs.update(this, msg));
	}

}

 

The parallel dispatcher will send the given message to each observer using as many parallel threads as reasonable:

 
package provided.util.dispatcher.impl;

/**
 * A dispatcher that updates its observers in parallel if possible on multiple threads.
 * @author swong
 *
 * @param <TMsg>  The type of message being sent to the observers.
 */
public class ParallelDispatcher<TMsg> extends ADispatcher<TMsg> {

	@Override
	public void updateAll(TMsg msg) {
		this.getObserverSet().parallelStream().forEach((obs) -> obs.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!

 

 


© 2019 by Stephen Wong