I was reviewing some code last week and came across a snippet that looked a lot like this:
1 2 3 4 5 6 7 8 9 10 11 12
My immediate response was to suggest that it didn’t make sense to be passing
four separate arguments to
someMethod, especially when the arguments were
being “unpacked” from an already-existing object. Certainly we could just pass
resp object directly to
someMethod, and let
someMethod unpack it as
necessary – we’d save some bytes, and we’d also leave ourselves some room to
grow. “I’m not a big fan of functions that take four arguments,” I said in my
To the original author’s credit, “because I say so” wasn’t sufficient reason to
rewrite code that was working just fine, thank you very much. If four arguments
was too many, was two arguments too many? Why draw the line at four? Surely the
four-argument signature helped indicate to future developers what was required
in order for the function to … function. Right? My hackles momentarily
raised, I parried by pointing out that if the arguments were actually required
by the function, maybe the function ought to actually check for their presence
before using them. Ha! While the original author was distracted by my disarming
logic, I fretted over the fact that I use a function that take four arguments
dojo.connect(node, 'click', contextObj, 'handlerMethod'). Ohnoes.
So where do you draw the line? Certainly you could write that
call like so:
1 2 3 4 5 6
This, though, might make you poke your eyes out. It certainly isn’t as concise as the four-argument approach, and it makes a lot of things like partial application a lot harder. Clearly there’s more to this than “if there’s more than four arguments, put them in an object” … but what are the rules?
Probably the most compelling reason to use an object is when there are several optional arguments. For example, last fall I was reviewing some code from a potential training client, and I came across this:
This case demonstrates the most compelling reason to switch to using an object
instead: optional arguments. When the developer discovered that the original,
addBling could be used for the five-argument case as well, it
was probably time to refactor:
1 2 3 4 5 6 7 8
Then, the same function could be used while passing it more information about how to behave in the five-argument case:
1 2 3 4 5 6
Then, when it came time to add yet more bling, the function signature wouldn’t need to change,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Extensibility and Future-Proofing
Another case for passing in an object is when you want the flexibility that an object provides, even if your code doesn’t require it for now:
1 2 3 4 5
For now, you only want to be able to provide the first and last name of the
person – it would work just fine to create a function signature for the
Person constructor that took exactly those two arguments, because indeed they
are required. On the other hand, though, this is incredibly short-sighted –
while first and last name may be all that you care about now, there’s obviously
more to a person than those two attributes, and eventually you may want to
provide attributes such as age, occupation, etc. Doing this with individual
arguments quickly becomes unsustainable. Besides that, though, it also makes
assigning instance properties a pain in the ass. By passing an object, we can
rewrite the above code as such:
1 2 3 4 5
Now – assuming this is what we want – we can mix in any settings we provide
args argument. Dojo, for example, bakes this ability in to anything
that inherits from
1 2 3 4
Use Objects for Related Data
An important qualifier here is that all of the properties of an object that
we’ve talked about passing to our
Person constructor are related – they all
are saying something about the Person you’re creating. What if creating our
Person was asynchronous, and we wanted to run a function once our Person was
created? In a (contrived) case like that, I think it does make sense to pass in
a separate argument:
In this particular example, we still only have two arguments – we haven’t
wandered into that muddy realm of four or more. That said, I think this
distinction is part of what makes
dojo.connect(node, 'click', contextObj,
'handlerMethod') OK: the arguments are four distinctly different types of
information. Taken together, they have an almost narrative quality: when this
node receives a click, use the context object’s
handlerMethod. A signature
new Person('Rebecca', 'Murphey', 34, 'web developer', 2 /*cats*/, 2
/*dogs*/) doesn’t feel the same as the
dojo.connect example – it’s
information that’s too related to be expressed as independent arguments.
Four or More, Time to Refactor?
I think the bottom line here is a) it’s complicated, and b) if your function
signature has four or more arguments, you should almost certainly consider
whether there’s a better way to do it. If the arguments are super-related, it
may be they belong in an object, so you get the benefit of easy extensibility
down the road. If there are optional arguments, you almost certainly want to
wrap those in an object to avoid passing
null over and over again.
Personally, my threshold is actually closer to two arguments – if I find myself wanting a third argument, I question whether my function is trying to do more than it should be doing – maybe I should do some pre-processing of the input so I can get away with just passing in two arguments. Every additional argument is an indication of additional complexity, which means an additional opportunity for things to go wrong.
I posed this question to Twitter and got a ton of interesting feedback. Here are some of the highlights that I didn’t cover above:
- @raydaly no new nouns is my principle. If unrelated data needs to be passed, diff args.
- @dadaxl I would pass an obj if I’ve a dynamic bunch of args containing functions.
- @sh1mmer omg! Objects for the love of god! No one likes immutable APIs. Just ask @ls_n
- @MattRogish Rails tends to do required things are named args, optional things are a hash
- @ryanflorence obfuscation often influences me, objects don’t compress as well as multiple args.
- @getify if more than half of the args are optional…or if there are several boolean params which without names can be confusing
- @jcoglan When further args are optional, or args.length>3. Need to spot when options merit a refactoring, though.
- @digitalicarus A combo of sheer length, amount of reuse, if it’s an API, and/or if it’s designed to be called a variety of ways to a similar end.
- @BryanForbes If I have to start swapping arguments and type checking, it’s time for one object or reworking my function.
- @myfreeweb I use an object when I start forgetting the order of args … or there is no logical order like (key, value, callback) at all
- @zetafleet When many of the arguments are optional or they’re all getting stored or copied directly over to the object.
- @maravillas I usually don’t make an obj just for passing data; if arglist is too long, maybe the function does too much and needs refactoring.
We ended up leaving the code that spurred this whole conversation exactly as it was.