Commands as Deferred Behavior

COMP 310    Java Resources  Eclipse Resources

[Back to Ballworld], [Back to Collisions]

One of the most common and useful applications of commands is to capture the specifics of a process but to then delay actually executing that process until some later time and/or place. The need to do this is characterized by a system that, at one point, knows exactly how to perform a process, e.g. has all or most of the necessary data to perform the process, but for some reason, is unable to actually perform the process at that moment. In such, the execution of that process must be delayed to another time or perhaps run by another entity somewhere else. Reasons for needed such a deferment include but are not limited to needing to wait for another process to complete or is still missing some critical piece of data.

In these situations, a command can be used to represent the process that needs to take place. When the system is able to construct the command, e.g. by having all or most of the necessary data, it can do so. Currying the necessary data or otherwise getting (e.g. via a constructor) the data into the closure of command enables the creation of a custom command specifically for that process with that data.

The command can then be saved for later execution or be passed to other parts of the system to be executed when necessary. The key point here is that whomever is executing the command later does not know or need to know any specifics of what that command is doing.

This techinque decouples the creation of the process (command) from the execution of the process.

 

Ball Collisions Interaction Behavior Example

When decoupling a ball's collision behavior, i.e. how its position and velocity are affected when "bouncing" off another ball, from the interaction criteria for determining whether or not the "bouncing" should take place (e.g. an overlap criteria), one soon discovers that allowing each ball to determine its own bouncing behavior is problematic because if the first ball being processed in the algorithm changes its velocity and location before the second ball has calculated the impulse value, that impulse value that is then calculated by the second ball will be incorrect.

There are a number of possible solutions/work-arounds to this issue, such as passing data between the balls to help eachother's calculations. Here we will explore how we could handle the issue using commands to defer the velocity and location changes until after both balls have calculated the impulse needed for those changes.

The first thing we need to do is to change the return type of an IInteractionStrategy from void to a command of some sort. IMPORTANT: The return type could have been either a Runnable or an IBallCmd. The latter was chosen simply because the infrastructure for its use already exists in the system but there are strong arguments for using a Runnable instead.

The interaction method of the strategy has been renamed below to "interactWithThen" to better reflect its new semantic of returning deferred behavior. The init() method needed for all strategies is also explicitly shown below.

	
@FunctionalInterface
public interface IInteractStrategy {

	/**
	 * Performs a directed interaction between the context ball and the target Ball from the 
	 * perspective of the context IBall.
	 * @param context  The IBall from whose perspective the interaction 
	 * processing takes place.
	 * @param target  The IBall that is the "other ball" in the perspective of this processing.
	 * @param disp  The Dispatcher that is to be used if desired.
	 * @return A command to be executed after both ball's interaction behaviors have completed.   
	 */
	public IBallCmd interactWithThen(IBall context, IBall target, IDispatcher<IBallCmd> disp);

	/**
	 * Default no-op initialization of the interact strategy. 
	 * While the semantics of a concrete method here are indeed problematic,
	 * defining an overridable default no-op method is a very common practice 
	 * for practical coding reasons.
	 * @param context   The context ball.
	 */
	default void init(IBall context) {
	}
}

 

Likewise, the IBall's interactWithThen method (renamed for the same reasons as above) should return the command generated by the interaction strategy.

	
	/**
	 * Invoke this ball's interaction strategy from the perspective of this ball.
	 * @param target  The "other" ball to interact with
	 * @param disp  A Dispatcher to use if desired.
	 * @return A command to be run by this ball after both balls' interaction behaviors are complete.
	 */
	public IBallCmd interactWithThen(IBall target, IDispatcher<IBallCmd> disp) {
		return this._interactStrategy.interactWithThen(this, target, disp);
	}

 

A typical interaction criteria code would then run the commands after both balls' interaction behaviors are complete:


	@Override
	public void updateState(final IBall context, IDispatcher<IBallCmd> dispatcher) {
		dispatcher.updateAll(new IBallCmd() {
			@Override
			public void apply(IBall other, IDispatcher<IBallCmd> disp) {
				if (interaction_criteria_satisfied) {   // whatever the interaction criteria is
				        // Save the commands that were generated.   Note that each ball is doing its own interaction behavior/calculations.
						IBallCmd contextPostInteractCmd = context.interactWithThen(other, disp);
						IBallCmd otherPostInteractCmd = other.interactWithThen(context, disp);
				        
						// Run the saved commands now that both balls' interaction behaviors are complete.
						// Notice that, as always, we delegate to the balls to execute the commands, not the other way around. 
				        context.update(disp, contextPostInteractCmd);
						other.update(disp, otherPostInteractCmd);
					}
				}
			}

		});

Now, the collision behavior simply needs to NOT perform the velocity and location changes but rather encapsulate those changes inside of the IBallCmd that it returns.

Typically, in situations where deferred behavior is needed:

Here is a description of most of code for a collide interaction strategy:

	public class CollideBehaviorStrategy implements  IIInteractionStrategy {

	// THIS CODE IS NOT COMPLETE -- IT IS ONLY A GUIDE FOR SOME OF THE REQUIRED PIECES 
	// TO CREATE ELASTIC COLLISION BEHAVIOR.  THE STUDENT IS EXPECTED TO IMPLEMENT ANY
	// MISSING PIECES!   

	@Override
	public IBallCmd interactWithThen(IBall source, IBall target, IDispatcher<IBallCmd> dispatcher) {
		// "source" ball is the "context" of the strategy.  The "target" ball is the ball receiving the command from the source ball's strategy.
		
		// TODO: To be completed by student.   
		
		/**
		
		Basic outline of the algorithm (use the utility functions below to calculate what you need) :
		
		1. Model the ball's "mass" as desired, e.g. proportional to ball's area
		2. It is handy to calculate the distance between the balls and the minimum non-colliding distance (= max colliding distance)
		3. Calculate the reduced mass of the system
		4. Calculate the unit vector pointing from the source ball to target ball
		5. Calculate the impulse vector for the source ball
		6. Calculate the "nudge" vector, which is the amount to move the source ball out of the way of the target ball after the collision.  
		7. Return an IBallCmd that:
			a. Update the velocity of the source ball using the impulse vector.  (The other ball will calculate its own impulse and update its own velocity)
			b. "Nudge" the source ball by translating its location by the nudge vector.   (The other ball will do it's own nudging.)

		*/  

	}
	
	// Utilities for calculating reduced mass, unit vector, impuylse and nudge vector elided.

}

Don't forget about the composite!

Remember that the composite interaction strategy is itself an interaction strategy, so if all interaction strategies' interact() methods have been defined as returning a command, so must the composite's interact() method. As always, a composite always delegates to its composees. But since each of the composee's are returning a command, the composite interaction strategy must return a single command that is effectively the composite of all the commands returned by the composees. That is, when the single command returned by the composite interaction strategy is run, that command should then run all of the commands returned by the composees.

The composite strategy needs to return a deferred command that runs the composees' returned deferred commands AFTER all the composee strategies are complete!

Here's a VERY common mistake when creating a composite interact strategy when deferred post-interaction commands are being returned. The following code will NOT operate correctly, e.g. it will cause colliding balls to quickly speed up and bounce off in the wrong directions. What is WRONG with this code and how do you FIX it?

// A new INCORRECT composite interact strategy:
new IInteractStrategy() {
		IInteractStrategy interactStrat1 // 1st composee.  Value set by unrelated code
		IInteractStrategy interactStrat2 // 2nd composee.  Value set by unrelated code
					
		@Override
		public IBallCmd interactWithThen(IBall host, IBall other, IDispatcher<IBallCmd> disp) {

			return new IBallCmd() {
			
				@Override
				public void apply(IBall host, IDispatcher<IBallCmd> disp) {
				
					IBallCmd cmd1 = interactStrat1.interactWithThen(host, other, disp);
					IBallCmd cmd2 = interactStrat2.interactWithThen(host, other, disp);
					host.update(disp, cmd1);   // Some may write this without properly delegating to the host to run the cmd: cmd1.apply(host, disp)  
					host.update(disp, cmd2);   // Some may write this without properly delegating to the host to run the cmd: cmd2.apply(host, disp)

					/**

					// A common variant of this error is the following, which is also incorrect on several levels: 
					interactStrat1.interactWithThen(host, other, disp).apply(host, disp);  
					interactStrat2.interactWithThen(host, other, disp).apply(host, disp);
					
					// or perhaps with more elegant incorrectness:
					
					host.update(disp, interactStrat1.interactWithThen(host, other, disp));
					host.update(disp, interactStrat2.interactWithThen(host, other, disp));

					*/
				}
			};

		}

		@Override
		public void init(IBall host) {
			interactStrat1.init(host);
			interactStrat2.init(host);
		}
	}

Hint: The error as written can be fixed with only a single cut-and-paste operation. The variants of the error mentioned above takes a little more work to fix but end up at the same solution.

 

Implementation Notes:

/**
 * Represents all IInteractStrategies that return no-op ball cmds as their deferred behaviors.
 */
public abstract class ANoOpCmdInteractStrategy implements IInteractStrategy {

	/**
	 * Simplified interaction method that performs a directed interaction between 
	 * the context IBall and the target IBall from the perspective of the context IBall.  
	 * There is no return value because none is needed since 
	 * all subclasses' interactWithThen() methods will return a Null command.
	 * @param context  The IBall from whose perspective the interaction 
	 * processing takes place.
	 * @param target  The IBall that is the "other ball" in the perspective of this processing.
	 * @param disp  The Dispatcher that is to be used if desired.
	 */
	public abstract void interactWith(IBall context, IBall target, IDispatcher<IBallCmd> disp);
	
	@Override
	public IBallCmd interactWithThen(IBall context, IBall target, IDispatcher<IBallCmd> disp) {
		this.interactWith(context, target, disp); // delegate to the simplified interaction method.
		return IBallCmd.NULL;  // Invariantly return a no-op command.
	}
}	  
	  
	  

Application in "Double-Pass" Simulations

This technique of using commands as deferred behavior can be used in certain types of "double-pass" simulations where the first "pass" of the algorithm over the data simply calculates all the changes that are needed for the data but does not actually change the data. The reason this is done is that if one piece of data was changed, then the subsequent calculations on other pieces of data, which may depend on the value of that first piece of data, would be adversely affected. The algorithm captures all the changes needed for the data in commands or some other sort of data structure and then, in a second "pass" over the data, performs all the changes.

Note that in many procedural style algorithms, the deferred behavior is often represented as saved data that directs later operations rather than as fully operational command objects. This can look different on the surface but in reality it is the same concept at work.

A classic example of a double-pass simulation is the classic Conway's Game of Life. It is also common to find this technique in neural net and lazy evaluation algorithms as well.

 

 

 

© 2020 by Stephen Wong