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
labelFunctionis 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
FullNameLabelResolveror aDateLabelResolverpacked 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
ILabelResolverinstance you can be sure that the rest will work.
Comments
Great post!
Even though I tend to favor strategies as well, I still see some benefits of untyped closures worth it to mention:
1. Reuse of static methods in static utility classes
2. Ability to inline the code (some performance enhancements at the prize of re-usability). it also gives the closure full access to its context
On these cases I’d really like to be able to use the Java syntax:
ILabelFunction closure = new ILabelFunction() {
public void labelFunction( TypedObject item ) {
//do whatever
}
};
renderer.labelFunction = closure;
We then have: inlining, strict typing and compiler checking, type inference and generics
Both C# delegates and Java closures are good solutions. I’d love to see them added in the following version of the language.
ActionScript3 suffers this dichotomy, it’s a powerful language but is kept simple enough so as to please the whole range of developers who are using it. Adobe has quite a tough job on achieving it…
Let’s see how it evolves.
Cheers!
That’s one of the things I like about haXe. You can define the signature of a function when you pass it as a parameter. Example:
function A(callback : String -> Bool) : Void;
That means that the callback parameter is a function that takes a string and returns a boolean. Put simply, the last bit is what the function returns, and the rest are parameters. So:
function A(callback : Int -> String -> Bool -> Void) : Void
means the callback has to take 3 parameters (int, string, bool) and return void. In case you might need to pass *any* function, you could use Dynamic (or go strategies, of course).
You guys have looked at haXe? The more I look at it (+Neko), the more I like it. With the new c++ backend and soonish targeting iPhone it gets more and more interesting.
Wonder why not more people is pick it!
That syntax is quite easy to follow actually. Nicolas Cannesse is a very pragmatic person.
I like the multi-language approach of haXe. Having one language that compiles to many bytecode types is smart (I didn’t know that the iPhone was haXe’s new target!)
Anyway, even though you have that particular feature available in the language, it is not supported natively by the Flash Player or even by JavaScript.
There must be a middleware library for each language that implements all those features. I think I’ll take a closer look :)