Saturday, June 6, 2009

ditch conditionals; use factories

When I first started coding, I used a lot of switch/case statements and complex ifs. But over the years, I become suspicious of them. Now, when I feel the urge to reach for a switch/case, I slap my wrist and try to come up with a better construct.

To demonstrate the problem, I've come up with a toy example: an app that fills the stage with little Fisher-Price-type people.



There are two types of people, bald people and hairy people. Just before placing a person on the stage, my app randomly chooses if he's going to have hair or not. Bald people are all members of the Person class; Hairy folks are members of the HairyPerson class. To create such classes, I do this:

var person1 : Person = Person( new bald() );
var person2 : Person = HairyPerson( new hairy() );

Note that I type both sorts of people to Person. I can do that because HairyPerson is a subclass of Person (and subclasses can be typed to their parent's type). By giving the vars the same type, I can pass them both into functions that expect a Person type, and those functions will work regardless of whether the person is a Person or a HairyPerson.

What are those new bald() and new hairy() calls doing for me? bald and hairy are MovieClips in my FLA's Library. Person classes must be passed a reference to a MovieClip. They use the clip as base artwork, which they then customize via random colors. If you want to know more about Person construction, you can check out my FLA and AS files, but the details aren't important to this post, so I won't elaborate here.

The question is this: in my Document class, where I randomly choose types of people, how do I instantiate type chosen at random? In the past, I would have done it this way:


for ( var i : int = 0; i < 100; i++ )
{
        rand = Math.floor( Math.random() * 2 );
        
        switch ( rand )
        {
                case 0 :
                        person = new Person( new bald() );
                        break;
                        
                case 1 :
                        person = new HairyPerson( new hairy() );
                        break;
                        
                
        }
        
        person.render();
        person.x = Math.random() * Stage.stageWidth;
        person.y = Math.random() * Stage.stageHeight;
        addChild( person );
}




The problem with this is that it doesn't scale well. If I add another type of person, say a HatWearingPerson, I need to add another case -- and I need to change my Math.random() statement so that it is range includes my new case:


for ( var i : int = 0; i < 100; i++ )
{
        rand = Math.floor( Math.random() * 3 ); //2 changed to 3
        
        switch ( rand )
        {
                case 0 :
                        person = new Person( new bald() );
                        break;
                        
                case 1 :
                        person = new HairyPerson( new hairy() );
                        break;
                        
                case 2 : //added
                        person = new HatWearingPerson( new hatWearer() );
                        break;
                
        }
        
        person.render();
        person.x = Math.random() * Stage.stageWidth;
        person.y = Math.random() * Stage.stageHeight;
        addChild( person );
}



This isn't such a big deal in my toy example, but imagine a more complex system in which you were constantly adding and removing types of people. Not only would I have to create the new classes; I'd also have to remember to add (or remove) new cases to the switch/case statement and to update the Math.random() statement. Eventually -- probably sooner rather than later -- I'm going to screw up and forget to add a case or forget to update Math.random().

But the bigger problem is that my Document class and the people classes are tightly coupled. Good OOP practice tells me that those person-instantiation statements shouldn't be hard coded into the Document class. Classes should be as independent from each other as possible, so that you wind up with a modular system in which you can change one class without having to change another.

What's the alternative? Factory classes! Factories are classes that make instances of other classes: Here are mine:


package
{
        public class PersonFactory implements IPersonFactory
        {
                public function getPerson() : Person
                {
                        var person : Person = new Person( new bald() );
                        return person;
                }
        }
}



package
{
        public class HairyPersonFactory implements IPersonFactory
        {
                public function getPerson() : Person
                {
                        var person : Person = new HairyPerson( new hairy() );
                        return person;
                }
        }
}



package
{
        public class HatWearingPersonFactory implements IPersonFactory
        {
                public function getPerson() : Person
                {
                        var person : Person = new HatWearingPerson( new hatwearer() );
                        return person;
                }
        }
}



As you can see, these classes all implement an interface called IPersonFactory. You'll see how this helps me out in a second. In the meantime, here's the interface:


package
{
        public interface IPersonFactory
        {
                function getPerson() : Person;
        }
}



My new and improved Document class looks like this:


var person : Person;
var rand : int;
var factories : Array;

addFactory( new PersonFactory ); //see addFactory(), below
addFactory( new HairyPersonFactory );
addFactory( new FancyPersonFactory );

for ( var i : int = 0; i < 100; i++ )
{
        rand = Math.floor( Math.random() * factories.length );
        person = factories[ rand ].getPerson();
        
        person.render();
        person.x = Math.random() * stage.stageWidth;
        person.y = Math.random() * stage.stageHeight;
        addChild( person );
}
                        

function addFactory( factory : IPersonFactory ) : void
{
        factories.push( factory );
}



Note that the addFactory function accepts any class that implements the IFactory interface, so my Document class doesn't know -- or need to know -- how many types of people (or people factories) exists.

From now on, if I add a new Person subclass, I just need to make a factory for it and pass that factory to my the Document class.

No comments:

Post a Comment