Long argument lists - who needs 'em?
Long argument lists are a pain. Using them can be a test of memory or an exercise in guess work. In recent years, attention has increasingly been paid to the usability of user interfaces, exploring how users actually use — or work around — user interfaces in practice, tracking how long certain tasks take or how many errors are made in completing a task. Similar considerations should apply to programmatic interfaces.
For example, to create a window using the Win32 API, the
CreateWindow function takes 11 arguments. Using this argument list is made that little bit more interesting because, in terms of underlying types, any one of the arguments can be called with a zero value.
Calls to such functions are often either cryptic or verbose. Position is significant and you have to remember what position has what meaning, so if you just write the call on one or two lines, and the arguments aren't suitably descriptive, you are left with something that is less than obvious. Which integer is which? Which handle is which? Which string is which? Sometimes, however, in attempting to deal with the problem, the resulting call is a less than compact ceremonial roll-call: each argument is passed on its own line, accompanied with a descriptive comment — 11 arguments take up 11 lines.
Tools can to some extent alleviate the call-site problem: context-sensitive help can offer you a hint when you are typing out the argument list. The quality of such feedback, however, can vary greatly with the development environment and the called library in question. There is still a problem in that after the act of initially writing the call and being away from the tool, such as browsing the code in a lightweight editor or just as plain text, you are still left with the underlying problem unsolved. Just flicking quickly through some Java should not require me to fire up Eclipse.
The question of readability is also not always obvious to the author defining — as opposed to calling — the argument list. In one example I came across, a method received five Boolean arguments that enabled or disabled various options. The author of the method wouldn't have noticed anything amiss as, in the body of the method, the role of each argument was quite clear from its name. At the point of call, however, you were typically left with a meaningless jumble of true and false literals — an example of code being written in, well, code. The
JTree.convertValueToText method is a comparable example from a published API.
One practical solution to the problem of multiple arguments with distinct meanings but the same declared type is to introduce meaningfully distinct types. In this case distinct enum types would make the call far more self-documenting and allow the compiler to catch any muddling of argument order. In the general case, using a distinct whole value, as opposed to a plain fundamental type, carries a lot more meaning and checkability.
There is also a methodological observation to make here: had the author of the method actually used the code, such as in a test case, the usability issue would probably have come to light sooner.
But if there is one thing that is more problematic with long argument lists than using them, it is maintaining their definition. Alan Perlis's epigram captures the problem succinctly:
"If you have a procedure with 10 parameters, you probably missed some".
CreateWindowEx, which takes 12 arguments, inadvertently demonstrates the point. What it adds to
CreateWindow is the ability to use extended window styles. It also demonstrates how constrained your choices are when you have a published interface in a statically typed language that does not support overloading. Adding an additional function with a different name is about the only thing you can do. If the distribution of your interface is more restricted and you can access all the points of use, you can potentially retain the name and add the argument(s), updating all the existing calls in the process. This is not without problems, and there is certainly an element of shotgun maintenance with each interface modification leading to changes scattered across the code base.
In addition to the question of usability, one of the issues we are dealing with here is that of interface stability in the face of change. If the language supports overloading you have more options available to you. Extended or modified argument lists can be accommodated by adding a new operation with the same name as the original but with an alternative argument lists. This ensures that the name and intent are stable, even if the detail of the argument list is not.
The same mechanism can also improve the usability of the interface. One of the most common problems with long argument lists is that, for common case uses, many of the values passed in are default values of some kind. Overloading allows the common case to be captured more directly, without having to contrive unmemorable names for different functions. It is, however, possible to get carried away with this ability and create a different problem. The API can become overloaded (in the classic sense of the term) and the programmer is bombarded with many operations that are identically named but subtly different.
Another tack is to address the core problem: reduce the number of arguments in the list. Remembering that one aspect of what we are dealing with is a question of stability — or, to be precise, a problem of volatility in search of a stable solution — we can think in terms of argument lists and cohesion. There are many criteria for cohesion, including common usage and stability, which can be used to guide how arguments are bundled together into coarser-grained parameter objects (also known as arguments objects).
Some groupings are obvious from the perspective of domain modelling: three integers representing year, month and day combine to give a date type; four integers representing the x and y position of a corner plus width and height combine to give a rectangle type; an upper and a lower bound combine to give a range; and so on. Other combinations are not necessarily as obvious, such as combining multiple Boolean arguments into an options object fitted out with methods for enabling and disabling each option specifically or, in its simplest form, a bitset with each option corresponding to a particular bit. Likewise, information required pervasively across an application or by plug-ins can be captured in one or more context objects rather than through global variables, which are commonly disguised as Singletons.
In terms of stability, consider an interface at the root of a class hierarchy that is declared to have a method that could potentially have many arguments. What is the effect of revisiting the decision to use, say, four arguments to add a fifth? This change will obviously touch the interface, but it will also break any code that depends on the interface, whether for usage or for implementation. This kind of ripple effect through a whole class hierarchy and its dependents is more like a tidal wave. Anyone overriding the method is affected, as is anyone calling it. In many cases a far simpler and more stable solution is to identify the grouping of certain arguments as more stable than the individual choice of arguments themselves. Any additions to such a parameter object leave the declared form of the method untouched and both clients and implementers unaffected.
Long argument lists are a pain for both callers and maintainers. What qualifies as long depends very much on how distinct the arguments are and how obvious their grouping appears — in some cases this can be as low as three or four arguments, but it is normally safe to assume that by the time you've hit 10 that is long by almost any definition!
It is possible to address many usability and stability issues through judicious use of techniques such as whole value types, overloading, and parameter objects. Language support for variable-length argument lists, optional arguments with default values and named, non-positional arguments offers further possibilities, some of which are beneficial and some of which come with their own problems. ®
Sponsored: Beyond the Data Frontier