Friday, November 6, 2009

Why You Should Override Clone In Event Subclasses

When I first learned how to code custom events in Actionscript 3.0, a friend told to override clone() when I subclassed flash.events.Event, but I didn't really understand why. So I ignored his advice.

It didn't seem to matter. I never wrote my own version of clone() and yet I never ran into any problems. But my friend's advice nagged at me. So I asked him why I should override clone(). He explained that if I didn't, clone() would return the standard Event type rather than my custom event type.

Let's say I create an Event subclass called KissEvent:


public calls KissEvent extends Event
{
public var withTongue : Boolean = true;

public static const BIG_SMOOCH : String = "bigSmooch";

...
}


As usual with events, I can set up a listener for KissEvent:

this.addEventListener( KissEvent.BIG_SMOOCH, onKiss );


function onKiss( kissEvent : KissEvent ) : void
{
...
}


The clone() problem occurs if I try to copy the Event via the clone() method it inherited from flash.events.Event:


function onKiss( kissEvent : KissEvent ) : void
{
var kissEventClone = kissEvent.clone();

trace( kissEvent.withTongue );
trace( kissEventClone.widthTongue );
}


The first trace, above, will output true. The second will cause an error. Flash will tell me that Event has no such property as withTongue, and in fact it doesn't. Sure, KissEvent has such a property, but clone() doesn't spit out KissEvents; it spits out Events.

public function clone() : Event

And you can't fool it this way:

trace( ( kissEventClone as KissEvent ).widthTongue );

Flash won't allow you to cast the clone as a KissEvent, because KissEvents MUST have withTongue properties. clone() spits out Events, which don't have withTongue properties, so there's no way Flash can convert an Event to a KissEvent. My variable name, kissEventClone, is misleading, because it really just holds an Event, not a kissEvent.

Once my friend explained this to me, I understood the problem, but I had a hard time caring about it. How often do I need to clone an event object? So far in my AS-coding experience, the answer is "never." The problem seems more theoretical than nuts-and-bolts actual.

But a year later, I encountered the REAL problem: double dispatches.

Let me explain: sometimes I create systems in which a listener forwards an event object to another listener. The first listener is a sort of middle-man between the dispatcher and the second listener. The middle man is both a listened and a dispatcher. It listens to the event, and, when it receives it, it dispatches the event to a second listener.

For example, say a game character has to listen for bullets coming at him. If a bullet hits him, he stops the bullet from flying any further and dies. That's the case UNLESS the character is a ghost. If a ghost gets hit by a bullet, it passes right trough him, so he just passes the event on to anything else that might be hit by it. Ghosts, like all characters, listen to Guns. Walls listen to Ghosts, because a Ghost might dispatch a bullet that passed through it, and that bullet might lodge in a wall.


event event
gun (dispatcher) --------------> ghost (dispatcher) ------------> back wall


Trouble is, a listener can't simply pass on an event. It can only pass on a cloned copy of the event. (That's a built-in "feature" of Flash's event system).


event event-clone
gun (dispatcher) --------------> ghost (dispatcher) -------------> back wall


Let's say that BulletEvents have a custom property called speed. Since clone() outputs Events (not BulletEvents), speed will be passed from gun to ghost but not from ghost to back wall.


event (speed=3) clone
gun (dispatcher) ---------------> ghost (dispatcher) -----------> back wall


So if the back wall tries to access speed, it's not going to find anything. Finally understanding the REAL problem, I made sure to override clone() in all my custom events. Here's a toy version of the solution:


public class MyEvent extends Event
{
public var myName : String;

public static const SOMETHING_HAPPENED : String = "somethingHappened";

public function MyEvent( type : String, myName : String,
bubbles : Boolean = true,
cancelable : Boolean = false )
{
this.myName = myName;
super( type, bubbles, cancelable );
}

override public function clone() : Event
{
return new MyEvent( super.type, myName, super.bubbles, super.cancelable );
}
}


Notice the overridden clone method. It returns a new MyEvent instance containing all the properties of the current instance. It truly IS a clone!

The one unfortunate thing is that clone() can't return a MyEvent. It must return an Event. That's because the clone() method in the superclass returns Event, and when you override a superclass's method, your new version must return the same type of object. But the returned Event is created by this statement: new MyEvent( super.type, myName, super.bubbles, super.cancelable ), so it DOES contain the myName property. This means we'll be able to use casting to get at myName, as I'll show below.

Now that we have our event, I'll show you the dispatcher/listen chain I have in mind:


event( w/myName ) clone( w/myname)
MyEventDispatcher ---------------> MyEventGrabbler (dispatcher) ------------> Main


Here's the code for MyEventDispatcher:


public class MyEventDispatcher extends EventDispatcher
{
public function dispatch() : void
{
var myEvent : MyEvent = new MyEvent( MyEvent.SOMETHING_HAPPENED, "Fred" );
dispatchEvent( myEvent );
}
}


Here's the middle-man code for MyEventGrabber:


public class MyEventGrabber extends EventDispatcher
{
private var _myEventDispatcher : MyEventDispatcher;

public function MyEventGrabber()
{
_myEventDispatcher = new MyEventDispatcher();
_myEventDispatcher.addEventListener( MyEvent.SOMETHING_HAPPENED, myHandler );
}

public function startTheEventBallRolling() : void
{
_myEventDispatcher.dispatch();
}

public function myHandler( myEvent : MyEvent ) : void
{
dispatchEvent( myEvent );
}
}


You might have expected the myHandler method to look like this:


public function myHandler( myEvent : MyEvent ) : void
{
dispatchEvent( myEvent.clone() );
}


While that's perfectly legal, it's not necessary. (Though maybe it's a good idea, as it makes what's happening explicit). When you re-dispatch an event, Flash uses clone() automatically.

Finally, here's the section of the Main class that catches the clone and accesses the myName property:


var myEventGrabber : MyEventGrabber = new MyEventGrabber();
myEventGrabber.addEventListener( MyEvent.SOMETHING_HAPPENED, myHandler );
myEventGrabber.startTheEventBallRolling();

function myHandler( event : Event ) : void
{
trace( (event as MyEvent).myName ); //Fred
}


Note that I use casting here to make Flash understand that the Event is actually a MyEvent. This casting only works because I overrode the clone() method in MyEvent.

There's one more potential problem that, admittedly, is less serious than the clone() problem. If I replace the above trace with this one...

trace( event );

... I won't see the myName property listed. Instead, I'll see this:

[Event type="somethingHappened" bubbles=true cancelable=false eventPhase=2]

The simple fix is to also override the toString() property in MyEvent, since when you hand trace() an object, it uses that object's toString() method. The simplest override is this:


override public function toString() : String
{
return "myName=" + myName;
}


But that will just trace out "myName=Fred", whereas what I want is...

[Event type="somethingHappened" bubbles=true cancelable=false eventPhase=2 myName="Fred"]

So I need to append the extra info to the end of the superclass's version of to string:


override public function toString() : String
{
return super.toString + " myName=" + myName;
}


Which gives me this:

[Event type="somethingHappened" bubbles=true cancelable=false eventPhase=2] myName=Fred

Because I'm anal, I need to get the myName=Fred inside the brackets. I also need to put quotation marks around "Fred," because it's a String. That's all easy to accomplish with a tiny bit of String manipulation. Here's my final version of MyEvent, with a robust toString() method. Note that I have also made a getter for myName, which stops random processes from being able to set myName to values that didn't come from MyEventDispatcher:


public class MyEvent extends Event
{
private var _myName : String;

public static const SOMETHING_HAPPENED : String = "somethingHappened";

public function MyEvent( type : String, myName : String,
bubbles : Boolean = true,
cancelable : Boolean = false )
{
_myName = myName;
super( type, bubbles, cancelable );
}

public function get myName() : String
{
return _myName;
}

override public function clone() : Event
{
return new MyEvent( super.type, _myName, super.bubbles, super.cancelable );
}

override public function toString() : String
{
var superString = super.toString();
var newProperties : String = ' myName="' + _myName + '"]';

//removes the closing bracket symbol from the end
//of the event-property list;
superString = superString.toString().substr( 0, superString.length - 1 );

return superString + newProperties;
}

}


The extra two methods, clone() and toString() add a lot of typing to the previously quick job of creating custom events. So I recommend that you use whatever templating system your IDE supports. If you're able to paste in generalized version of those method, it only takes a few seconds to make the necessary changes to support your particular event.

No comments:

Post a Comment