It's a pain to have to change the parameters of a function; you have to change every other call to that function in order to avoid errors. But you can get around this by using just one parameter: a configuration object.
What It Looks Like
Here's a silly example of a function for creating a robot:
function generateRobot(arms:int, personality:String):Robot { var robot:Robot = new Robot(); for (var i:int = 0; i < arms; i++) { //create arm and add it to robot } if (personality == "evil") { robot.commands = "Destroy mankind."; } else { robot.commands = "Bake cookies." } return robot; } generateRobot(2, "evil");
Now, here's the same example, using a configuration object:
function generateRobot(conf:Object):Robot { var robot:Robot = new Robot(); for (var i:int = 0; i < conf.arms; i++) { //create arm and add it to robot } if (conf.personality == "evil") { robot.commands = "Destroy mankind."; } else { robot.commands = "Bake cookies." } return robot; } generateRobot({arms:2, personality:"evil"});
I've highlighted the lines that require changing; you can see that there's not much difference.
Why Bother?
So if there's hardly any difference, why would we bother doing it the second way? After all, it actually makes the function a little harder to use; whereas before our IDE would be able to give us this information about the parameters the function expected:
...now it can only give us this:
Suppose you want to add a couple more parameters: one specifying the material to use and another to specify what color its laser should be. That's not too hard, in either case:
function generateRobot(arms:int, personality:String, material:String, laserColor:String):Robot { var robot:Robot = new Robot(); for (var i:int = 0; i < arms; i++) { //create arm and add it to robot } if (personality == "evil") { robot.commands = "Destroy mankind."; } else { robot.commands = "Bake cookies." } switch (material) { case "wood": //wooden robot break; case "steel": default: //steel robot break; } robot.laser = new Laser(); robot.laser.color = laserColor; return robot; } generateRobot(2, "evil", "steel", "red");
function generateRobot(conf:Object):Robot { var robot:Robot = new Robot(); for (var i:int = 0; i < conf.arms; i++) { //create arm and add it to robot } if (conf.personality == "evil") { robot.commands = "Destroy mankind."; } else { robot.commands = "Bake cookies." } switch (conf.material) { case "wood": //wooden robot break; case "steel": default: //steel robot break; } robot.laser = new Laser(); robot.laser.color = conf.laserColor; return robot; } generateRobot({arms:2, personality:"evil", material:"steel", laserColor:"red"});
So far, still not much of a difference. What if you want your robots to all have red lasers by default? Simple again. Without a configuration object, you just need to change the method signature (the function
line), and then you can remove the last argument from the function call:
function generateRobot(arms:int, personality:String, material:String, laserColor:String = "red"):Robot { //this is all the same } generateRobot(2, true, "steel"); //I removed the last argument
With a configuration object, it's a little trickier - though not much:
function generateRobot(conf:Object):Robot { if (!conf.laserColor) { conf.laserColor = "red"; } var robot:Robot = new Robot(); for (var i:int = 0; i < conf.arms; i++) { //create arm and add it to robot } if (conf.personality == "evil") { robot.commands = "Destroy mankind."; } else { robot.commands = "Bake cookies." } switch (conf.material) { case "wood": //wooden robot break; case "steel": default: //steel robot break; } robot.laser = new Laser(); robot.laser.color = conf.laserColor; return robot; } generateRobot({arms:2, personality:"evil", material:"steel"}); //I removed the last argument
Okay. Now suppose you find that you're setting almost all of your robots to be evil (I mean, why not?), so it's actually kind of a pain to write "evil" as a parameter every time. Naturally, you want to set "evil" as the default - but you don't want to set a default material.
The only way you can do this, with a regular set of function parameters, is to switch the order of the personality
and material
parameters:
function generateRobot(arms:int, material:String, personality:String = "evil", laserColor:String = "red"):Robot {
Ah, but now you have to switch the order of the arguments round on every single function call!
generateRobot(2, "evil", "steel"); //no longer works
A configuration object doesn't give you this problem. Check it out:
function generateRobot(conf:Object):Robot { if (!conf.laserColor) { conf.laserColor = "red"; } if (!conf.personality) { conf.personality = "evil" } //this is all the same } generateRobot({arms:2, material:"steel"}); //no "personality" parameter? no problem!
Neat! All your old generateRobot()
function calls will continue to work, but you can create new calls that don't bother specifying personality
.
You can even decide to get rid of the personality
parameter altogether:
function generateRobot(conf:Object):Robot { if (!conf.laserColor) { conf.laserColor = "red"; } if (!conf.personality) { conf.personality = "evil" } var robot:Robot = new Robot(); for (var i:int = 0; i < conf.arms; i++) { //create arm and add it to robot } robot.commands = "Destroy mankind."; switch (conf.material) { case "wood": //wooden robot break; case "steel": default: //steel robot break; } robot.laser = new Laser(); robot.laser.color = conf.laserColor; return robot; }
The above version of the function doesn't refer to conf.personality
at all - but you won't get an error if you still have calls like this:
generateRobot({arms:2, personality:"evil", material:"steel"});
Of course, you might get a few confused users if you have calls like this:
generateRobot({arms:2, personality:"good", material:"steel"});
...since all robots are now evil. But at least the code will compile.
For the same reason, you can change the order of the arguments without it mattering at all, and even add in new parameters that don't do anything yet:
generateRobot({material:"steel", laserColor:"green", arms:2, voice:"Mr. T"});
Making It Easier to Set Defaults
The code for setting the defaults is easy to understand so far, but is going to be very annoying to extend if we need to have lots of parameters:
if (!conf.laserColor) { conf.laserColor = "red"; } if (!conf.personality) { conf.personality = "evil" }
Let's write some more general code to cope with it:
var defaults:Object = { laserColor:red, personality: "evil" } for (var key:String in defaults){ if (!conf[key]) { conf[key] = defaults[key]; } }
That for
loop may be a little confusing, so I'll break it down. First, look at this:
for (var key:String in defaults){ trace(key); }
This is a for...in
loop, which will output the names of the keys inside the default
object:
laserColor personality
Next, look at this line:
trace(defaults["laserColor"]);
This will output red
- it's the same as writing trace(defaults.laserColor)
.
Following on from that, look at this example:
var example:Object = { demo: "test" }; trace(example["demo"]); trace(example["foo"]);
What do you think this will output?
Well, example["demo"]
is the same as example.demo
, which equals "test"
. But example.foo
does not exist, so example["foo"]
will return null
. This means that !example["foo"]
(note the exclamation mark) will be equivalent to true
.
Put that all together, and you should be able to understand why this code works:
var defaults:Object = { laserColor:red, personality: "evil" } for (var key:String in defaults){ if (!conf[key]) { conf[key] = defaults[key]; } }
Give me a shout in the comments if you need a hand!
I Want More!
For an even quicker version, try this:
function generateRobot(conf:Object = null):Robot { var conf:Object = conf || {}; var defaults:Object = { laserColor:red, personality: "evil" } for (var key:String in defaults){ conf[key] = conf[key] || defaults[key]; }
The change in Line 1 (and new Line 2) means that even the conf
object itself is optional, so you can just call generateRobot()
. (Of course, you'll need to change the code to deal with the values that don't currently have defaults.)
Helping the IDE Help You
As I mentioned above, the IDE can't give you any tips about what parameters a function is expecting, if that function uses a configuration object. This is a major drawback, as it can make your code really hard to use; you have to remember which parameters go in the conf
object, as well as all of their names and types.
But we can still display this information to the coder when it's needed; we just have to do so manually, like so:
/** * Generate a robot, based on the parameters given. * @param conf Configuration object. Expects: * arms (int) Number of arms robot should have. * personality (String) Personality of robot. Can be "evil" or "good". Defaults to "evil". * material (String) What the robot should be made out of. Can be "steel" or "wood" at this time. * laserColor (String) Color of the robot's laser. Defaults to "red". * voice (String) Vocal stylings of robot. Currently not implemented. * @return The finished robot. */ function generateRobot(conf:Object):Robot { // }
Now, if I start to write a call to this function in FlashDevelop (my IDE of choice), I see this:
Sure, it's a bit of a pain to keep this manually updated, but in many cases it's worth it.
Conclusion
I'm not claiming that you should use a configuration object for every single function you create from now on; just think of it as another useful tool in your arsenal.
Personally, I find it a particularly useful pattern whenever I'm building the first draft of some set of classes that all need to work together. The added flexibility of a conf
gives me so much more flexibility, freeing me up to zip around all the different functions and changing how they call one another, without worrying about breaking the code by inserting or removing a parameter.
Remember the benefits:
- It's easy to add and remove parameters (at either end).
- It's easy to set default values.
- You don't have to worry about the order of the parameters.
There are drawbacks to using simple objects like I have, though - especially if you do so in a project that's past the prototyping stage. Check out the great comments below for more details!
Comments