Sometimes a function starts out with a reasonable number of parameters, but as the code grows and more features are added, it ends up with more and more — like PixelDisplay.drawImage in Mini Micro, which has 10 parameters.
Arguments in a function call in MiniScript are passed by position, not by name, which means that even if some of those parameters are optional, you have to specify all arguments that come before one you actually care about. And of course you have to remember what order they're in. (So many times I have typed @gfx.saveImage at the Mini Micro prompt just to remind myself of the order of the parameters!)
Languages that support passing parameters by name encourage developers to make functions with lots of parameters. And then it will turn out that only certain combinations of those parameters make sense, and the function sprouts pages of parameter-checking logic just to ensure that the user is calling it properly, and more pages of documentation or comments to help them do so. This is not just academic; I had to work with this 25-parameter monster for a while, and it was a nightmare. I have rarely seen any language feature so quickly lead to bad code as named parameters; it may even take the lead from GOTO in this department.Why not passing parameters by name is a good thing
What can be done when you find yourself needing more than a handful of parameters? Or, what if it's only a handful, but you find yourself frequently passing the same values from one function to another (helper methods for example)? There is a standard solution: the Introduce Parameter Object refactoring.
Introduce Parameter Object
The idea here is simple: take that big batch of parameters you need, and replace them with a single object containing the same information.
As an example, the PixelDisplay.drawImage function looks like this:
FUNCTION(self, image, left=0, bottom=0, width=-1, height=-1, srcLeft
=0, srcBottom=0, srcWidth=-1, srcHeight=-1, tint="#FFFFFF")
Not counting self (which is just whatever object comes before the dot when you call it), that's 10 parameters. You need all 10 if you're going to tint the image, like this:
img = file.loadImage("/sys/pics/Block.png")
gfx.drawImage img, 500, 200, -1, -1, 0, 0, -1, -1, color.aqua
(I encourage you to fire up Mini Micro, either locally or on the web, and follow along for yourself.)
Let's make a parameter-object version of this method. (In your own code, you could actually replace the original method with the refactored one; but since you can't change the Mini Micro API, let's just make a new method.) Use the edit command and enter:
import "mapUtil" // for map.get
drawImage = function(params)
g = params.get("gfx", gfx)
g.drawImage params.image,
params.get("left", 0),
params.get("bottom", 0),
params.get("width", -1),
params.get("height", -1),
params.get("srcLeft", 0),
params.get("srcBottom", 0),
params.get("srcWidth", -1),
params.get("srcHeight", -1),
params.get("tint", "#FFFFFF")
end function
Close the editor and run.
Now we have a drawImage method that takes a single parameter. You can call it like this:
params = {}
params.image = img
params.left = 500
params.bottom = 200
params.tint = color.aqua
drawImage params
Notice how this time, we only needed to specify the parameters that differ from the defaults. We could also call it using a map literal, like this:
drawImage {"image":img, "left":600, "bottom":200, "tint": color.pink}
That works too. Either way, the calling code is more clear, and the function interface is simpler.
Output Parameters
There's a related pattern that is helpful when you need to (perhaps optionally) provide some extra detail on the result of a function call, and that is to use an output parameter object.
To make it more concrete, let's suppose you make a method that loads an image... but you want it to do some extra sanity checks, and report details on what it found to callers who care. You could write it like this:
import "mapUtil" // for map.get
loadImage = function(path, outResults)
r = outResults // (shorthand)
if r != null then
r.path = path
r.size = 0
r.err = ""
end if
info = file.info(path)
if info == null then
if r then r.err = "File not found"
return null
end if
if r then r.size = info.size
directory = file.parent(path)
name = file.name(path)
if name[-4:] != ".png" and name[-4:] != ".jpg" then
if r then r.err = "Path does not end with .png or .jpg"
return null
end if
if file.children(directory).indexOf(name) == null then
if r then r.err = "Case mismatch"
return null
end if
return file.loadImage(path)
end function
This loadImage method takes the path to the function to load, but optionally, it also takes a map into which we will place some details: the path, the file size, and an error string.
So callers who don't care about the details can just do:
img = loadImage("/sys/pics/Wumpus.png")
...and they will get back the image or null, but with no way of knowing why. More careful callers could instead do:
result = {}
img = loadImage("/sys/pics/Wampus.png", result)
if not img then print "Couldn't load " + result.path +
" because: " + result.err
And (because I mistyped the image name) this will print "Couldn't load /sys/pics/Wampus.png because: File not found".
(We might make use of exactly this pattern in some Mini Micro 2 APIs; see this feature request.)
Don't Over Do It!
There are two drawbacks to introducing parameter objects:
- They make calling the function more of a hassle for simple cases.
drawImage img is easier than drawImage {"image":img}.
- They encourage parameter proliferation, just like named parameters.
Don't create beasts with 20 or 30 parameters, even with this refactoring. Instead consider smaller, more focused methods that do only one thing in one way. Or at the very least, add wrapper methods that make the common use cases easy.
But with those caveats in mind, parameter objects (including output parameter objects) are a powerful tool in your toolbox. Happy coding!
Top comments (0)