7 Comments

Another option, if the same list of parameters is used in many places, think about grouping them into one meaningful struct/object.

for example instead of `get_users(offset int, limit int, sort_by string)` -> `get_users(filter obj)`

Expand full comment

That's exactly the refactoring I posted as part of Tidy Together? last week. Reordering params is a tidying--little, quick, generally safe, unlikely to affect others.

Expand full comment

Got it, seen the overview, but not a paid subscriber yet :)

Expand full comment

The "meaningful" part is extremely important. Creating a new kind of object for the sole purpose of grouping parameters, even if they seem related and tend to be passed together, always makes the code worse. The only thing it does is hide the loose parameters in a "bag", but they are still there and accessed in the same way. The "bag" introduces an indirection without doing any abstraction work, so it's a net negative.

But the more important issue is that having the same list of parameters passed through multiple layers of functions is a clear red flag that the functions fail to accomplish their most basic task: abstraction. Each function's implementation generally dips down into lower level dependencies so the caller from the higher level never has to know about them. If they don't do that, and keep passing around the same parameters, they don't give any abstraction benefits, and the whole structure of the system needs rethinking. But if you try to hide parameter lists behind objects, this becomes that much harder to see.

The focus of this kind of advice needs to be about identifying system structure problems, otherwise it does more harm than good.

Expand full comment

"always makes the code worse" I disagree, both from a coupling/cohesion perspective & from experience. These kinds of emergent objects often become important, both for programmers & for domain experts.

Expand full comment

Definitely valuable (and as Kent said, mentioned before), but there are some cases where the parameters that serve similar purposes across a loosely related family of functions shouldn't naturally be bundled together, or at least that feels awkward to me, and so "canonical" order helps you remember how to use / read these functions.

A concrete example might be the `itertools` functions in python, which often accept an iterator (roughly, a lazy stream, if you're not familiar with the terminology). The *usually* (but sadly not always) take the iterator argument last, ie

- map(func, iterator): apply the function to each item in the stream

- filter(predicate, iterator): only pass on items that don't keep the stream

- takewhile(predicate, iterator): pass on the prefix of the stream for which predicate is true

and so on. This sort of consistency is quite helpful in my view.

Expand full comment

One of the principles behind argument order design in Clojure is that sequence functions take the sequence as the last argument but collection/object functions take the collection/object as the first argument. A sequence in Clojure is an abstraction similar to an iterator in some other languages, distinct from a (concrete) collection/object type.

Another principle is to order arguments so ones that change least come first, which makes it easier to make a partial application of the function (like currying). And, yes, sometimes this will conflict with the other principle so you have to decide which is most important based on how the function will be used.

As you say, consistency is helpful!

Expand full comment