Refactoring closures to strategies

by Joan Llenas

Sometimes you need to expose a feature of your API as a function reference in order to allow the execution of that feature in a custom way.
This kind of functions are known as closures.
A closure is basically a function reference that is executed within a particular scope, being able to access any scope member through argument passing.
This feature is very powerful as it enables the injection of external logic into an encapsulated scope, but has the drawback that there’s no way of knowing whether a given function will satisfy its target closure’s signature.
Languages like C# have a feature called delegates that enable closures to be type safe. ActionScript does not, so we have to make use of other OOP features in order to overcome the limitations that the use of closures impose us.

A good closure use case can be found in the ListBase APIs of the Flex SDK.
The ListBase.labelFunction property expects a function reference which, when assigned by the user, will parse every object within the list’s collection, providing the custom logic needed in order to render a correct representation of the iterating object.
Let’s see an example: (You can download the completed example)

API:

package
{
	import flash.text.TextField;
 
	public class ItemRenderer extends TextField
	{
		private var defaultLabelField:String = "label";
		public var labelFunction:Function;//closure
 
		public function setData( data:Object ):void{
			if( labelFunction == null ){
				this.text = data[defaultLabelField];
			}else{
				this.text = labelFunction( data );
			}
		}
	}
}

You can see that in this case the closure is the labelFunction property.
User code using the API (with closures):

package
{
	import flash.display.Sprite;
 
	public class ClosureExample extends Sprite
	{
		public function ClosureExample()
		{
			var renderer:ItemRenderer = new ItemRenderer();
			renderer.x = renderer.y = 50;
			renderer.setData( {label:"The Label"} );
			addChild( renderer );
 
			var closure:Function = function( data:Object ):String
			{
				return String( data["altLabel"] );
			}
			renderer = new ItemRenderer();
			renderer.x = renderer.y = 100;
			renderer.labelFunction = closure;
			renderer.setData( {altLabel:"Alternative Label"} );
			addChild( renderer );
		}
	}
}

The above code adds two ItemRenderer instances to the display list, where the second one uses the closure in order to render its label.
This snippet above is absolutely correct, but let’s see whether we can improve its readability.
Problems that I see with this approach:

  • Users will have to check the documentation from time to time in order to remind the closure’s implementation details (closure signature).
  • The only way of being sure that the labelFunction is correct is by running the affected piece of code where the closure is called.

What we need in order to overcome this issues is a way of enforcing the closure’s signature at compile time.
This can be achieved by using what is known as a Strategy, one of the design patterns defined in the GoF book.

A strategy is an algorithm plugged into an object in an encapsulated way.
Both closures and strategies do help with the externalization of application logic, that’s why they can be exchanged very easily as they have the same purpose and behave in the same way.
Let’s see how the refactored code looks like after replacing closures with strategies:

User code using the API (with strategies):

package
{
	import flash.display.Sprite;
 
	import labelResolvers.DateLabelResolver;
	import labelResolvers.FullNameLabelResolver;
 
	public class TypeSafeClosure extends Sprite
	{
		public function TypeSafeClosure()
		{
			var renderer:ItemRenderer = new ItemRenderer();
			renderer.x = renderer.y = 25;
			renderer.setData( {label:"The Label"} );
			addChild( renderer );
 
			renderer = new ItemRenderer();
			renderer.x = renderer.y = 50;
			renderer.labelResolver = new FullNameLabelResolver( "name", "surname" );
			renderer.setData( {name:"Joan", surname:"Llenas"} );
			addChild( renderer );
 
			renderer = new ItemRenderer();
			renderer.x = renderer.y = 75;
			var df:DateFormatter = new DateFormatter( DateFormatter.FULLYEAR, DateFormatter.MONTH, DateFormatter.DAY );
			renderer.labelResolver = new DateLabelResolver( df, "date" );
			renderer.setData( {date:new Date(1976,2,12)} );
			addChild( renderer );
		}
	}
}

APIs:
ItemRenderer

package
{
	import flash.text.TextField;
 
	import labelResolvers.ILabelResolver;
 
	public class ItemRenderer extends TextField
	{
		private var defaultLabelField:String = "label";
		public var labelResolver:ILabelResolver;
 
		public function setData( data:Object ):void{
			if( labelResolver == null ){
				this.text = data[defaultLabelField];
			}else{
				this.text = labelResolver.resolve( data );
			}
		}
	}
}

ILabelResolver

package labelResolvers
{
	public interface ILabelResolver
	{
		function resolve(data:Object):String;
	}
}

FullNameLabelResolver

package labelResolvers
{
	public class FullNameLabelResolver implements ILabelResolver
	{
		private var nameDataField:String;
		private var surnameDataField:String;
 
		public function FullNameLabelResolver( nameDataField:String="name", surnameDataField:String="surname" )
		{
			this.nameDataField = nameDataField;
			this.surnameDataField = surnameDataField;
		}
 
		public function resolve(data:Object):String
		{
			return data[nameDataField] +" "+ data[surnameDataField];
		}
	}
}

DateLabelResolver

package labelResolvers
{
	public class DateLabelResolver implements ILabelResolver 
	{
		private var dateField:String;
		private var formatter:DateFormatter;
 
		public function DateLabelResolver( formatter:DateFormatter, dateField:String="date" )
		{
			this.dateField = dateField;
			this.formatter = formatter;
		}
 
		public function resolve(data:Object):String
		{
			return formatter.format( data[dateField] );
		}
	}
}

As you can see there’s a little bit more code to write; not much more anyway; but the implementation is better in some aspects:

  • Strategies encourage reuse: i.e. It has sense having a FullNameLabelResolver or a DateLabelResolver packed in a library.
  • No need to look at the documentation: The auto complete functionality will give you all the details you may need.
  • Compile time error checking: The closure signature is abstracted by the strategy implementation, so as far as you pass the correct parametrization to the ILabelResolver instance you can be sure that the rest will work.
Refactoring Closures to Strategies
Refactoring Closures to Strategies (47)
v.0.1
42.09 KB