Wednesday, August 5, 2009

on the use of constructors

I've tried constructing objects in every way imaginable. After reading many books, blog-posts and essays about best-practices, I was swayed by people who argued that the all data an object needs should be fed to it via its constructor. Before coming to that conclusion, I used to write lots of parameter-initialization methods.


public class Spaceship extends Sprite
{
private var _hullColor : uint;
private var _hasLaser : Boolean;
private var _laserColor : uint;

... more private properties ...

public function set hullColor ( value : uint ) : void
{
_hullColor = value;
}

public function set hasLaser( value : uint ) : void
{
_hasLaser = value;
}

public function set laserColor( value : uint ) : void
{
_laserColor = value;
}

... more setter methods ...

public function render() : void
{
... code that draws the space ship ...
}
}


Using this method, I'd create a Space-ship instance as follows:


var ussValiant : SpaceShip = new SpaceShip();
ussValiant.hullColor = 0x666666;
ussValiant.hullShape = HullShape.CIGAR;
ussValiant.hasLasers = true;
ussValiant.laserColor = 0x00cc00;
ussValiant.hasBridge = true;
ussValiant.bridgeColor = 0x666666;
ussValiant.bridgeShape = BridgeShapes.SPHERE;
ussValiant.bridgeSize = BridgeSizes.large;

ussValiant.render();


The trouble here is that it's impossible to tell whether or not I need to set all the parameters or if some of them are optional. Do I need to set them in a particular order? What happens if I set one after calling render? To even know about all the parameters, I have to read through the whole class's API. If, instead, I set all necessary parameters via a constructor, a quick glance at its heading will tell me everything I need to know about parameter order and requirements:


public function SpaceShip( hullColor : uint, hullShape : uint, hasLasers : Boolean = false,
laserColor : uint = 0, hasBridge : Boolean = false,
bridgeColor : uint = 0, bridgeShape : String = "sphere",
bridgeSize = "small" )


The obvious downside to this approach is the unwieldy number of parameters that are stuffed inside the constructor's parentheses. It was this very problem that originally convinced me to write all those setters. I hate overstuffing a constructor, because constructors should take, at most, three parameters.

Why three? Well, it's not a hard and fast rule. It's just an aesthetic. Three, because it's relatively easy to remember three things. After that, it gets harder, at least for my poor, addled brain. Constructors that take four or more parameters are hard to use and hard to read without confusion. Compare the following:

THE BEST KIND OF CONSTRUCTOR:
var puppy : Puppy = new Puppy();

A VERY GOOD CONSTRUCTOR:
var name : String = "Fred";
var monster : Monster = new Monster( name );

A PERTTY GOOD CONSTRUCTOR:
var bread : String = BreadTypes.RYE;
var meat : String = MeatTypes.TURKEY;
var hasMayo : Boolean = true;
var sandwich : Sandwich = new Sandwhich( bread, meat, hasMayo );

A DUBIUS CONSTRUCTOR:
var hasMilk : Boolean = false;
var hasSugar : Boolean = true;
var isIced : Boolean = false;
var size : String = CoffeeSizes.GRANDE;
var coffee : Coffee = new Coffee( hasMilk, hasSugar, isIced, size );

A TERRIBLE CONSTRUCTOR:
var title : String = "Lonesome Parrot";
var genre : GenreTypes.WESTERN;
var stars : Array = [ "Ian McShane", "Michelle Pfeiffer" ];
var director : String = "Stephen Spielberg";
var writer : String = "Thomas Kenneally";
var budget : Number = 50000000;
var hasSequel : Boolean = true;
var movie : Movie = new Movie( title, genre, stars, director, writer, budget, hasSequel );

When faced with the prospect of a terrible constructor, it's tempting to just pass it a value object:


public class MovieData
{
public var title : String;
public var genre : String;
public var stars : Array;
public var director : String;
public var writer : String;
public var budget : Number;
public var hasSequel : Boolean = true;
}


The benefit here is that the constructor now only needs to take in one parameter:

var movie : Movie = new Movie( movieData : MovieData );

The problem with this approach is that the constructor has lost its power to communicate. From looking at its signature, all I know is that it takes in some vague sort of data. I have to look at the value-object class to learn more. And looking at that still doesn't tell me whether or not I need to give every parameter a value or if some are optional.

Sometimes, this is the best I can do. But usually, I can solve this problem by breaking up the class into several smaller classes. In fact, when I feel the need to pass a constructor many parameters -- via raw values or through a value object -- I take it as a sign that my thinking isn't granular enough.

For instance, does it really make sense to have just one monolithic movie class? At the very least, I can break it up into Movie, Metadata and Staff:


public class Staff
{
...

public function Staff( director : String, writer, String, stars : Array )
{
...
}

...
}

public class Metadata
{
...

public function Metadata( title : String, genre : String, budget : Number,
hasSequel : Boolean = false )
{
...
}

...
}

public class Movie
{
...

public function Movie( metadata : Metadata, staff : staff )
{
...
}
}


I'm not crazy about the Metadata class, because it has four parameters, which violates my aesthetic limit. Perhaps I'll leave it as is, though. I'm not dogmatic about the three-parameter rule. However, looking at the parameters, I do have a vague feeling that "budget" is a different sort of beast from "title", "genre" and "hasSequel". I might consider creating a new class called Finances or at least keep that option open in my mind. I know for sure that I don't want to add any more parameters to Metadata! Maybe Metadata itself should take in three parameters, category (title and genre), hasSequels and finances( budget ). Or maybe Metadata should only be about the title, genre and sequels. Maybe Movie should take in metadata, staff and finances.

Regardless of my ultimate decision, my gaol is to keep everything small: small number of parameters, small methods and small classes. Small is good, because small is readable; small is easy to maintain.

This super-modular approach mirrors the way we think. If I ask you to tell me about your best friend, you might say, "His name is Bob. He's a lab technician." Translated to OOP, that's...

var bestFriend : Person = new Person( "Bob", "lab technician" );

But what if I ask you to describe Bob in more detail. You're likely to say something like, "Well, he's been married for ten years and he has a little girl named Sarah. He has brown hair and green eyes. He lives in Seattle." Notice how you broke down your description into categories. You're didn't amorphously describe Bob. You specifically told me about his family, his looks and his location.

You're didn't do this:

var bestFriend : Person = new Person( "Bob", "lab technician", 10, "Sarah", "brown", "green", "Seattle" );

You did this:

var family : Family = new Family( 10, ["Sarah"] );
var looks : Looks = new Looks( "green", "brown" );
var location : Location = new Location( "Seattle" );
var bestFriend : Person = new Person( "Bob", "Lab Technician", family, looks, location );

That's two too-many parameters, but it's more readable and natural than the former version. Maybe name, occupation and looks could be bundled into a class called Description. That would give you...

var bestFriend : Person = new Person( description, family, location );

The goal isn't to come up with the perfect solution. There isn't a perfect solution. The gaol is to continually play around to make your code as simple and communicative as possible.

No comments:

Post a Comment