What’s the deal with Signals?

7 07 2011

Signals. Heard of them? What’s the big deal you say?

Simple. The event system in AS3 is both limited and antiquated. True, native AS3 events offer a convenient way of messaging (bubbling) withinUI hierarchies. Yet, at an abstract API level, they more often as not restrict the developer than aid them.

Chiefly, what Robert Penner has done with as3-signals is create a way to represent events as variables, rather than as magical strings firing off at the type (class) level. It sounds simple. It is. Yet the implications for your architecture is vast.

Consider the following interface of asynchronous methods:

public interface IServiceStream
{
  function open():void;
  function close():void;
}

Now, as the contract is asynchronous, we’ll need some events to notify us when methods have completed. Let’s say we have the following events:

  • OPENED
  • CLOSED
  • ERROR
  • TIMEOUT

In keeping with the native AS3 model, the best we can hope for is using the following metadata at the type level:

[Event(name="streamOpened",type="...")]
[Event(name="streamClosed",type="...")]
[Event(name="streamError",type="...")]
[Event(name="streamTimeout",type="...")]
public interface IServiceStream
{
 //...
}

There are four problems with this approach:

  1. Decorating via metadata does not enforce that implementors of the interface actually dispatch these events.
  2. We should, for completeness, define the events somewhere as static constants. This means we can no longer simply write interfaces, and need to write event implementations and deploy them with our API;
  3. We’re using magic strings, and as there is no compile-time checking of the metadata, we’re opening ourselves up to illusive runtime errors, if the wrong events are dispatched.
  4. There is nothing to specify which events fire when – and which events belong to which method, and which belong to the class itself.
The first two are fairly straightforward, so let’s focus on the latter two.

Magic Strings and No Contract

We have no way of tying the event type to the constant in some Event class it will eventually correspond to. “streamOpened” may map to ServiceStreamEvent.OPENED, and yet we cannot know this at the metadata level (not for the interface or even the implementor). From #1, it is evident that although we can put these requirements in, we cannot enforce their usage.

Method vs Type-level Events 

Anyone listening to an implementor of our interface, would listen at the type level for all events, and deal with them as they occurred.

For example:

var service:IServiceStream = new ServiceStreamImplementation(...);

service.addEventListener(ServiceStreamEvent.OPENED, function(evt:Event):void { ... } );
service.addEventListener(ServiceStreamEvent.CLOSED, function(evt:Event):void { ... } );
service.addEventListener(ServiceStreamEvent.ERROR, function(evt:Event):void { ... } );
service.addEventListener(ServiceStreamEvent.TIMEOUT, function(evt:Event):void { ... } );

//later when required
service.open();
We’ve been forced to declare all our handlers in one point, early enough to precede the calling of any event-dispatching methods. Anyone reading the code will have no real knowledge at which point the implementing class dispatches which event – hence why all the listeners need to be adding initially. As the interface writer, all we can do is say “this interface can dispatch any of these events” – we cannot even enforce that they are used. From #1 above, the metadata is not enforced, it’s just decoration.

Here is where Signals come in. Let’s rewrite the interface using simple signals.

public interface IServiceStream
{
  function open():ISignal;
  function close():ISignal;
}
Now let’s look at a partial implementation.
import mx.rpc.events.ResultEvent;
import mx.rpc.http.HTTPService;

import org.osflash.signals.ISignal;
import org.osflash.signals.Signal;

public class ServiceStream implements IServiceStream
{
	public function open():ISignal
	{
		var signal:Signal = new Signal(Object);

		//do something asynchronously...
		var httpService:HTTPService = new HTTPService();
		httpService.addEventListener(ResultEvent.RESULT,
			function(evt:ResultEvent):void
			{
				//use the closure to access your signal and dispatch it async
				signal.dispatch(evt.result);
			});

		httpService.send();

		return signal;
	}

	//function close();
}

Now, the usage of this implementation can become:

var service:IServiceStream = new ServiceStreamImplementation(...);

service.open().addOnce(function(result:Object):void
{
    //do something with your returned "result"
});

In one fell swoop we fixed all four of the problems with events. We even have the convenience methods addOnce() and removeAll() from the ISignal interface. The former ensures your listener is removed after it is first used, the latter is pretty self-explanatory. If you look even closer, you’ll see we just implemented the Fluent interface for free.

Imagine this in your mediator pattern – your UIs by definition have no reference to their mediator. Now they have a prescribed way of notifying their mediators that something has occurred.

Wait a second. What about those other events?

How do you return multiple items from a regular method call – compose a type for your requirements.

You could write the following signal collection:

public class ServiceSignals
{
	public var open:ISignal = new Signal(Object);
	public var error:ISignal = new Signal(String);
	public var timeout:ISignal = new Signal();
}

and change your interface to:

public interface IServiceStream
{
  function open():ServiceSignals;
  //...
}

Better yet, you could keep your interface and simply conform your Signal collection into an ISignal with a default listener/dispatcher:

public class ServiceSignal extends Signal
{
	var open:ISignal = new Signal(Object);
	var error:ISignal = new Signal(String);
	var timeout:ISignal = new Signal();

	override public function add(listener:Function):ISignalBinding
	{
		return open.add(listener);
	}

	override public function addOnce(listener:Function):ISignalBinding
	{
		return open.addOnce(listener);
	}

	override public function dispatch(...parameters):void
	{
		open.dispatch();
	}

	override public function remove(listener:Function):ISignalBinding
	{
		return open.remove(listener);
	}

	override public function removeAll():void
	{
		open.removeAll();
	}
}

Then you could use it as such:

var service:IServiceStream = new ServiceStreamImplementation(...);

var signal:ServiceSignal = service.open();

signal.addOnce(function(result:Object):void
{
    //do something with your returned "result"
});

signal.error.addOnce(...);

signal.timeout.addOnce(...);

Perhaps you’re not a huge fan of this solution. You may find that the error & timeout signals are type-level events, and you don’t want to have to add handlers for both open() and close(). OK – so what about this implementation?

public class ServiceStream implements IServiceStream
{
	public var error:ISignal = new Signal(String);
	public var timeout:ISignal = new Signal();

	private var _time:int = 30000;
	private var timer:Timer;

	public function open():ISignal
	{
		var signal:Signal = new Signal(Object);

		//do something asynchronously...
		var httpService:HTTPService = new HTTPService();
		httpService.addEventListener(ResultEvent.RESULT,
			function(evt:ResultEvent):void
			{
				//use the closure to access your signal and dispatch it async
				signal.dispatch(evt.result);
			});

		httpService.addEventListener(FaultEvent.FAULT,
			function(evt:FaultEvent):void
			{
				error.dispatch(evt.fault.faultString);
			});

		timer = new Timer(_time,1);

		var timerHandler:Function = function(evt:TimerEvent):void
			{
				timeout.dispatch();
				timer.removeEventListener(TimerEvent.TIMER_COMPLETE, timerHandler);
			}
		timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerHandler);

		httpService.send();

		timer.start();

		return signal;
	}

	//function close();

}

Notice how we define the handler function as a variable so we can remove it in the listener. This is replicating the ISignal.addOnce() functionality. True, we could have used weak event listeners to allow for garbage collection, however this way is closer to our approach with Signals, so we’ll keep it for consistency.

Your implementor could then be used like this:

var service:IServiceStream = new ServiceStreamImplementation(...);

service.open().addOnce(function(result:Object):void
{
    //do something with your returned "result"
});

service.error.addOnce(function(message:String):void
{
    //handle error
});

service.timeout.addOnce(function():void
{
    //handle timeout
});

service.close().addOnce(function():void
{
    //now closed
});

Whichever way you decide, Signals give you the choice you need to make the best decision for your API.


Actions

Information

5 responses

30 07 2011
This Fortnight on Twitter and Google+ | Flash Video Traning Source

[…] Karl Macklin’s screencast introduction to Signals that we published recently? Augment it with this article by Justin J […]

7 08 2011
UI mediation sucks. Mediate behaviours, not views. « Justin J. Moses : Blog

[…] Signals provide an alternative to events. They are designed to be used within APIs and class libraries, rather than replacing events altogether (Flash events are well suited to UI hierarchies). Where events fire and are handled at a type (class) level, signals live at the variable level. Not only can we pass them to and return them from methods, we can also enforce their presence in types that implement our interfaces. Check out an earlier post on Signals. […]

19 08 2011
Cleaning up after closures in Flash « Justin J. Moses : Blog

[…] is a final alternative – use the as3-signals library. AS3 Signals is a library that provides an alternative to using Flash Events within your […]

21 03 2012
internet pro | Pearltrees

[…] You could write the following signal collection: public class ServiceSignals { public var open:ISignal = new Signal(Object); public var error:ISignal = new Signal(String); public var timeout:ISignal = new Signal(); } How do you return multiple items from a regular method call – compose a type for your requirements. and change your interface to: Wait a second. What about those other events? What’s the deal with Signals? « Justin J. Moses : Blog […]

5 10 2013
Benoit Jadinon

your ServiceSignal is a pretty nifty idea.
it was kinda limited though, here’s a version that can be subclassed and that is not tied to String as a return value :

public class ServiceSignal extends Signal
{
public var error:ISignal = new Signal(String);
public var timeout:ISignal = new Signal();
public function get success():ISignal
{
return this;
}

function ServiceSignal(…parameters){
super(parameters);
}
}

Leave a comment