Recently, while reading the HTML5 Doctor interview with Ian Hickson, when asked what some of his regrets have been over the years, the one he mentions, rather comically so as being his “favorite mistake”, also happened to be the one which stood out to me most; that is, his disappointment with pushState; specifically, the fact that of the three arguments accepted, the second argument is now ignored.
I can empathize with his (Hixie’s) frustration here; not simply because he is one of the most influential figures on the web – particularly for his successful work surrounding CSS, HTML5, and his responsibilities at the WHATWG in general – but rather, it is quite understandable how such a seemingly insignificant design shortcoming would bother such an obviously talented individual, especially considering the fact that pushState's
parameters simply could not be changed due to the feature being used prior to completion. Indeed, the Web Platform poses some very unique and challenging constraints under which one must design.
While the ignored pushState
argument is a rather trivial issue, I found it to be of particular interest as I often employ Parameter Objects to avoid similar design issues.
Parameter Objects
The term “Parameter Object” is one I use rather loosely to describe any object that simply serves as a wrapper from which all arguments are provided to a function. In the context of JavaScript, object literals serve quite well in this capacity, even for simpler cases where a function would otherwise require only a few arguments of the same type.
Parameter Objects are quite similar to that of an “Options Argument” – a pattern commonly implemented by many JavaScript libraries to simplify providing optional arguments to a function; however, I tend to use the term Parameter Objects more broadly to describe a single object parameter from which all arguments are provided to a function, optional arguments included. The two terms are often used interchangeably to describe the same pattern. However, I specifically use the term Options Argument to describe a single object which is reserved exclusively for providing optional arguments only, and is always defined as the last parameter of a function, proceeding all required arguments.
Benefits
Parameter Objects can prove beneficial in that they afford developers the ability to defer having to make any final design decisions with regard to what particular inputs are accepted by a function; thus, allowing an API to evolve gracefully over time.
For instance, using a Parameter Object, one can circumvent the general approach of implementing functions which define a fixed, specific order of parameters. As a result, should it be determined that any one particular parameter is no longer needed, API designers need not be concerned with requiring calling code to be refactored in order to allow for the removal of the parameter. Likewise, should any additional parameters need to be added, they can simply be defined as additional properties of the Parameter Object, irrespective of any particular ordering of previous parameters defined by the function.
As an example, consider a theoretical rotation function which defines five parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /* * Performs a rotation on the given element based on the * provided values, delegating to the respective native * CSS3 rotation functions. * * @param {String} el id of the element to rotate * @param {Number} x the x-axis of the rotation * @param {Number} y the y-axis of the rotation * @param {Number} z the z-axis of the rotation * @param {Number} a the angle of the rotation */ var rotate = function(el, x, y, z, a){ ... }; |
Using a Parameter Object, we can refactor the above function to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /* * Performs a rotation on the given element based on the * provided values, delegating to the respective native * CSS3 rotation functions. * * @param {Object} params * params.el {String} id of the element to transform * params.x {Number} the x-axis of the rotation * params.y {Number} the y-axis of the rotation * params.z {Number} the z-axis of the rotation * params.a {Number} the angle of the rotation */ var rotate = function(params){ ... }; |
Should we wish to remove a parameter from the function, doing so simply requires making the appropriate changes at the API level without changing the actual signature of the function (assuming of course, there are no specific expectations already being made by calling code regarding the argument to be removed). Likewise, should additional parameters need to be added, such as a completion callback
, etc., doing so, again, only requires making the appropriate API changes, and would not impact current calling code.
Additionally, taking these potential changes as an example, we can also see that with Parameter Objects, implementation specifics can be delegated to the API itself, rather than client code insofar that the provided arguments can be used to determine the actual behavior of the function. In this respect, Parameter Objects can also double as an Options Argument. For example, should the arguments required to perform a 3D rotation be omitted from the Parameter Object, the function can default to a 2D rotation based on the provided arguments, etc.
Convenience
Parameter Objects are rather convenient in terms of there being less mental overhead required than that of a function which requires ordered arguments; this is especially true for cases where a function defines numerous parameters, or successive parameters of the same type.
Since code is generally read much more frequently than it is written, it can be easier to understand what is being passed to a function when reading explicit property names of an object, in which each property name maps to a parameter name, and each property value maps to parameter argument. This can aid in readability where it would otherwise require reading the rather ambiguous arguments passed to a function. For example:
1 2 3 4 | // call with specific arguments rotate('#div', 0.7, 0.5, 0.7, 45); |
With Parameter Objects it becomes more apparent as to which arguments correspond to each specific parameter:
1 2 3 4 5 6 7 8 9 10 | // call with a Parameter Object rotate({ 'el': '#div' , 'x': 0.7 , 'y': 0.5 , 'z': 0.7 , 'a': 45 }); |
As mentioned, if a function accepts multiple arguments of the same type, the likelihood that users of the API may accidentally pass them in an incorrect order increases. This can result in errors that are likely to fail silently, possibly leading to the application (or a portion thereof) becoming in an unpredictable state. With Parameter Objects, such unintentional errors are less likely to occur.
Considerations
While Parameter Objects allow for implementing flexible parameter definitions, the arguments for which being provided by a single object, they are obviously not intended as a replacement for normal function parameters in that should a function need only require a few arguments, and the function’s parameters are unlikely to change, then using a Parameter Object in place of normal function parameters is not recommended. Also, perhaps one could make the argument that creating an additional object to store parameter/argument mappings where normal arguments would suffice adds additional or unnecessary overhead; however, considering how marginal the additional footprint would be, this point is rather moot as the benefits outweigh the cost.
A Look at pushState’s Parameters
Consider the parameters defined by pushState
:
- data: Object
- title: String
- url: String
The second parameter, title
, is the parameter of interest here as it is no longer used. Thus, calling push state requires passing either null
or an empty String
(recommended) as the second argument (i.e. title
) before one can pass the third argument, url
. For example:
1 2 3 4 | // invoke pushState with each argument pushState({'state':'some state'}, '', 'some.html'); |
Using a Parameter Object, pushState
could have been, theoretically, implemented such that only a single argument was required:
- params: Object
- data: Object
- title: String
- url: String
Thus, the ignored title
argument could be safely removed from current calling code:
1 2 3 4 5 6 7 | // invoke pushState with only relevant arguments pushState({ 'data' : {'state':'some state'} , 'url' : 'some.html' }); |
And simply ignored in previously implemented calls:
1 2 3 4 5 6 7 8 | // invoke pushState with relevant arguments and deprecated arguments pushState({ 'data' : {'state':'some state'} , 'title' : 'Some Title' , 'url' : 'some.html' }); |
As can be seen, the difference between the two is quite simple: the specification for pushState
accepts three arguments, whereas the theoretical Parameter Object implementation accepts a single object
as an argument, which in turn provides the original arguments.
Concluding Thoughts
I certainly do not assume to understand the details surrounding pushState
in enough detail to assert that the use of a Parameters Object would have addressed the issue. Thus, while this article may reference pushState
as a basic example to illustrate how the use of a Parameter Object may have proved beneficial, it is really intended to highlight the value of using Parameter Objects from a general design perspective, by describing common use-cases in which they can prove useful. As such, Parameter Objects provide a valuable pattern worth considering when a function requires flexibility.