Friday, September 25, 2009

procedural programming in an OOP world

Procedural programming is one damn thing after another. The one place I tend to write procedural code is in initialization modules. For instance, say I'm making a video player. The player needs to first load in an xml file that contains configuration info (width, height, background color, etc); then it needs to load in another xml file, containing a playlist; next it needs to load in external graphics; then it needs to render everything to the stage; finally, it needs to play the first video in the playlist.

What I want to write is something like this:


function init() : void
{
loadConfigData();
parseConfigData();
loadPlaylistData();
parsePlaylistData();
loadGraphics();
renderToStage();
playVideo( 0 );
}


Alas, asynchronous-loading makes this impossible. My code can't proceed directly from the first step, loadConfigData(), to the second step, parseConfigData(), because it needs to pause and wait for the data to load before it can begin parsing. So I'm forced to use listeners and callback methods.


function loadConfigData() : void
{
var urlLoader : URLLoader = new URLLoader();
urlLoader.addEventListener( Event.COMPLETE, parseConfigData );
urlLoader.load( "config.xml" );
}

function parseConfigData( event : Event ) : void
{
var urlLoader : URLLoader = event.currentTarget as URLLoader;
var loadedData : String = urlLoader.data;
var xml : XML = XML( loadedData );

...

loadPlaylistData();
}


My nice, neat, compact procedural list becomes an ugly chain of functions that call other functions:


function stepA() : void
{
//do stuff
stepB();
}

function stepB() : void
{
//do stuff
stepC();
}

function stepC() : void
{
//do stuff
stepD();
}

...


This sucks, because in order to get a bird's-eye-view of the initialization procedure, I have to scroll through all the code. If I want to stop a function from running -- say during debugging -- I need to search to find where that function is being called. It would be so much easier if I could just do this:


function init() : void
{
loadConfigData();
parseConfigData();
loadPlaylistData();
parsePlaylistData();
//loadGraphics();
//renderToStage();
//playVideo( 0 );
}


I decided to solve this problem by creating a class called StepManager, which allows you to run asynchronous code as if it's synchronous. StepManager takes care of the pause between each step. It knows to run step B only after step A has completed.

It's very easy to use. You create a (usually) small class for each step. Then you feed the steps to StepManager like this:


var stepManager : StepManager = new StepManager();
stepManager.addNextStep( StepA );
stepManager.addNextStep( StepB );
stepManager.addNextStep( StepC );


When you're done adding steps, you start them running like this:


stepManager.exectute();


If you want to be informed of when all the steps are complete, you can add an event listener to stepManager:


var stepManager : StepManager = new StepManager();
stepManager.addEventListener( StepEvent.COMPLETE, stepsCompleteHandler );
stepManager.addNextStep( StepA );
stepManager.addNextStep( StepB );
stepManager.addNextStep( StepC );

function stepsCompleteHandler( event : StepEvent ) : void
{
trace( "All steps complete." );
}


To disable a step, you simply comment out the line of code where you added it to the StepManager:


var stepManager : StepManager = new StepManager();
stepManager.addNextStep( StepA );
stepManager.addNextStep( StepB );
//stepManager.addNextStep( StepC );


Note that StepA, StepB and StepC are classes NOT instances. The StepManager creates instances from the classes you hand it. You just create the classes. Here's the bare bones of what a step class looks like:


import com.grumblebee.utilties.stepmanager.AbstractStep;

public class StepA extends AbstractStep
{
public function StepA( previousStep : AbstractStep = null )
{
super( previousStep );
}

override public function execute() : void
{
//do stuff
super.stepComplete();
}
}


So to make a step, you just extend AbstractStep, write a simple constructor that takes in one argument (previousStep), and override a function called execute. The guts of your step -- whatever you want the step to do -- go in execute().

Each step MUST announce when it's done. It does so by calling super.stepComplete(); (Behind the scenes, stepComplete dispatches an event that's received by the StepManager. When it gets that event, StepManager knows it's okay to move onto the next step.)

Note the previousStep parameter in the constructor. By default, it's set to null. This handles the case of the first step (StepA). There's no step before it, so there can't be a previousStep. But StepManager sends each subsequent step the step that came before it. That way step B can extract any information it needs from step A; Step C can extract information from step B and so on.

Here's a more complete example:

Step A holds url of a text file, which it hands to step B. Step B loads in the contents of the text file, which it hands to step C. Step A looks like this:


public class StepA extends AbstractStep
{
public function StepA( previousStep : AbstractStep = null )
{
super( previousStep );
}

public function get urlToLoad() : String
{
return "external.txt";
}

override public function execute() : void
{
trace( "Step A: It knows the URL of the text file: " + urlToLoad );
super.stepComplete();
}
}


Note that the new method, urlToLoad(). It's public, so that step B can access it. Step B is a little more complicated. It looks like this:


public class StepB extends AbstractStep
{
private var _loadedData : String;

public function StepB( previousStep : AbstractStep = null )
{
super( previousStep );
}

public function get loadedData () : String
{
return _loadedData;
}

public function set loadedData( value : String ) : void
{
_loadedData = value;
trace( "Step B: storing this loaded data: " + _loadedData );
}

override public function execute() : void
{
var stepA : StepA = ( previousStep ) as StepA;
var url : String = stepA.urlToLoad;

trace( "Step B. received this URL from Step A: " + url );

var urlLoader : URLLoader = new URLLoader();
urlLoader.addEventListener( Event.COMPLETE, loadCompleteHandler );
urlLoader.load( new URLRequest( url ) );
}

function loadCompleteHandler( event : Event ) : void
{
var urlLoader : URLLoader = event.currentTarget as URLLoader;
urlLoader.removeEventListener( Event.COMPLETE, loadCompleteHandler );

loadedData = urlLoader.data;

super.stepComplete();
}
}


Most of the extra stuff here is just the code to load data from an external text file. One detail worth noting how step B gets the url from step A:


var stepA : StepA = ( previousStep ) as StepA;
var url : String = stepA.urlToLoad;


Another detail you should note is that step B stores the data from the external file in a variable called _loadedData. It allows the next step to access that data via a getter method:


public function get loadedData () : String
{
return _loadedData;
}


Step C looks like this:


public class StepC extends AbstractStep
{
public function StepC( previousStep : AbstractStep = null )
{
super( previousStep );
}

override public function execute() : void
{
var stepB : StepB = previousStep as StepB;
trace( "Step C: received text from step B: " + stepB.loadedData );
super.stepComplete();
}
}


Note in its execute() method how step C extracts data from step B.

As shown previously, we can now run all these steps like this:


var stepManager : StepManager = new StepManager();

stepManager.addNextStep( StepA );
stepManager.addNextStep( StepB );
stepManager.addNextStep( StepC );

stepManager.execute();


Here's the output:

Step A: It knows the URL of the text file: external.txt
Step B. received this URL from Step A: external.txt
Step B: storing this loaded data: Hi, mom!
Step C: received text from step B: Hi, mom!
All steps complete.


In the future, I plan to extend StepManager -- allowing branching as-well-as pause and resume functions. In the meantime, you can download the code (and examples) here:

http://www.grumblebee.com/grumblecode/StepManager.zip

Please let me know if you have any questions or suggestions.

No comments:

Post a Comment