Monday, August 3, 2009

adobe's mistake: displayObjects need interfaces

Last week, I wrote a function that drew a randomly-colored circle on a Sprite:


function randomColoredCircle( target : Sprite ) : void
{
var color : uint = Math.round( Math.random() * 0xFFFFFF );
var xPosition : Number = 0;
var yPosition : Number = 0;
var radius : Number = 30;

target.graphics.beginFill( color );
target.graphics.drawCircle( xPosition, yPosition, radius );
target.graphics.endFill();
}


To call this function, all I had to do was hand it a Sprite to draw on:


var sprite : Sprite = new Sprite();

randomColoredCircle( sprite );

addChild( sprite );


This worked fine until I suddenly needed to draw a circle on a MovieClip. I couldn't hand a MovieClip to the function, because it specifically requests Sprites:


function randomColoredCircle( target : Sprite ) : void //won't accept a MovieClip


NOTE: a commenter pointed out that MovieClips do, in fact, inherit from the Sprite class, so the above function will work. However, as you'll see (below), the Shape class does not extend Sprite (or vice versa), so the problem remains.

The worst possible solution to this problem is to make another version of the function that's exactly the same, except it draws on MovieClips:

BAD:


function randomColoredCircleOnSprite( target : Sprite ) : void { ... }
function randomColoredCircleOnMovieClip( target : MovieClip ) : void { ... }


Using this approach, I might have to make a third function that draws circles on Shapes. Ugh! This is begging for trouble. What if I decide to make the circles smaller? I would have to remember to update "var radius : Number = 30" in all three functions.

Here's a better solution -- one I would advocate if Actionscript wasn't an object-oriented langiage:


function randomColoredCircleOnSPRITE( target : Sprite ) : void
{
var canvas : Graphics = target.graphics;
randomColoredCircle( canvas );
}

function randomColoredCircleOnMOVIECLIP( target : MovieClip ) : void
{
var canvas : Graphics = target.graphics;
randomColoredCircle( canvas );
}

function randomColoredCircle( canvas : Graphics ) : void
{
var color : uint = Math.round( Math.random() * 0xFFFFFF );
var xPosition : Number = 0;
var yPosition : Number = 0;
var radius : Number = 30;

canvas.beginFill( color );
canvas.drawCircle( xPosition, yPosition, radius );
canvas.graphics.endFill();
}


This works by having separate functions for Sprite and MovieClip objects. All those functions do is to extract Graphics objects from their owners and send the Graphics to another function, one that actually does the drawing. This works, because though MovieClips and Sprites are different classes, a Graphic is a Graphic is a Graphic. I can now easily draw on Shapes by making one more graphic-extraction function:


function randomColoredCircleOnSHAPE( target : Shape ) : void
{
var canvas : Graphics = target.graphics;
randomColoredCircle( canvas );
}


Though this approach moves in the right direction, we can do better. In an object-oriented system, if you want to do the same thing to two related classes, you do it to their parent class. In fact, both MovieClip and Sprite descend directly from DisplayObject. So, in theory, I should be able to elegantly solve my problem (and eliminate all the graphics-extractor classes) by drawing on DisplayObjects instead of MovieClips and Sprites:


function randomColoredCircle( target : DisplayObject ) : void
{
var color : uint = Math.round( Math.random() * 0xFFFFFF );
var xPosition : Number = 0;
var yPosition : Number = 0;
var radius : Number = 30;

target.graphics.beginFill( color );
target.graphics.drawCircle( xPosition, yPosition, radius );
target.graphics.endFill();
}


The variable target can accept any object in the DisplayObject family, so I can call this function as follows:


var sprite : Sprite = new Sprite();
var movieClip : MovieClip = new MovieClip();

randomColoredCircle( sprite );
randomColoredCircle( movieClip );


Unfortunately, this WON'T WORK. I'll get an error telling my that DisplayObjects don't have a property called "graphics." In fact, they don't. This is because though MovieClips, Shapes and Sprites have graphics properties, there are other DisplayObjects that don't. For instance, TextField is a DisplayObject descendant that doesn't have a graphics property. You can't draw on a TextField.

The definition of DisplayObject looks like this:


public class DisplayObject
{
private var _x : Number;
private var _y : Number;
...
//Note: no _graphics property!
}


MovieClip, Sprite and Shape extend DisplayObject like this:


public class MovieClip extends DisplayObject
{
...
private var _graphics : Graphics;
}


TextField extends it like this:


public class TexTField extends DisplayObject
{
private var _embedFonts : Boolean;
private var _text : String;
...
//Note: no _graphics property!
}


So though you can store both a Sprite and a MovieClip in a variable typed to DisplayObject, you can't access the graphics property of that variable, because DisplayObjects don't have graphics.

However, since several of DisplayObject's children DO have graphics properties, you should be able to refer to all those children generically via a common interface. In other words, what Adobe SHOULD have done -- but didn't -- is to define an IDrawable interface like this:


public interface IDrawable
{
//returns the private _graphics property
function get graphics () : Graphics;
}


Then, it should have implemented this interface for MovieClip, Sprite and Shape:


public class MovieClip extends DisplayObject implements IDrawable
{
...
}

public class Sprite extends DisplayObject implements IDrawable
{
...
}

public class Shape extends DisplayObject implements IDrawable
{
...
}


Had Adobe just added that little bit of extra code, we could type variables as IDrawable and not have to worry about whether they were Sprites, MovieClips or Shapes:


var sprite : IDrawable = new Sprite();
var moveClip : IDrawable = new MovieClip();
var shape : IDrawable = new Shape();

randomColoredCircle( sprite );
randomColoredCircle( movieClip );
randomColoredCircle( shape );


The last step to making this work is to rewrite the randomColoredCircle function so that it accepts IDrawable objects:


function randomColoredCircle( target : IDrawable ) : void
{
var color : uint = Math.round( Math.random() * 0xFFFFFF );
var xPosition : Number = 0;
var yPosition : Number = 0;
var radius : Number = 30;

target.graphics.beginFill( color );
target.graphics.drawCircle( xPosition, yPosition, radius );
target.graphics.endFill();
}


This works because ALL IDrawable objects have a graphics property:


public interface IDrawable
{
//returns the private _graphics property
function get graphics () : Graphics;
}


Like I said, we COULD have used this method IF Adobe had just implemented a common interface for all the classes with graphics properties. They should have done this, as it's a standard and good programming technique, but they didn't. However, all is not lost, because we can do it ourselves by extending the MovieClip, Sprite and Shape classes and making sure our new classes implement IDrawable.

Below, I'll show the complete code to do this. It's a small amount of code, involving some empty classes. The classes are empty because we don't want to add anything to them other than the fact that they implement IDrawable. In other words, MySprite is exactly the same as Sprite, except for the fact that MySprite implements IDrawable and Sprite doesn't:


package
{
import flash.display.Graphics;

public interface IDrawable
{
function get graphics() : Graphics
}
}

package
{
import flash.display.Sprite;

public class MySprite extends Sprite implements IDrawable
{

}
}

package
{
import flash.display.MovieClip;

public class MyMovieClip extends MovieClip implements IDrawable
{

}
}

package
{
import flash.display.Shape;

public class MyShape extends Shape implements IDrawable
{

}
}


That's it. Now I just need to use MyShape, MySprite and MyMovieClip instead of Shape, Sprite and MovieClip, and I'll be guaranteed that all of my classes will implement a common interface:


var sprite : MySprite = new MySprite();
var movieClip : MyMovieClip = new MyMovieClip();
var shape : MyShape = new MyShape();

randomColoredCircle( sprite );
randomColoredCircle( shape );
randomColoredCircle( movieClip );


MyShape, MySprite and MyMovieClip have all properties and methods of Shape, Sprite and MovieClip (width, rotation, etc.), because they inherit those properties from the classes they extend.

Even if you don't ever need to treat Sprites, Shapes and MovieClips as if they are all the same type of objects, I hope my approach shows off the usefulness of common interfaces. If you have several classes that share similar traits, it's key that you relate those classes via inheritance and/or an interface. Doing so will make your code much more extendible for future projects.

2 comments:

  1. Except that MovieClip DOES extend Sprite.. your first function should have worked fine.

    http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/display/MovieClip.html

    ReplyDelete
  2. Thanks for noticing that. I've amended the article.

    ReplyDelete