How to Design Function Parameters That Make Interfaces Easier to Use (1/3)
When you look at an function in an interface, 3 prominent things give you indications about how to use it: its name, its parameters and its return type. And when you look at a piece of code calling that function, it’s just its name and its function parameters.
We’ve already covered in details how to give good names to the components of your code. Now we’re going to examine how to design function parameters in a way that both your interfaces and the code that calls them are as expressive as can be.
Summed up in one sentence, you want to make the decision of what arguments to pass to your functions a no-brainer.
There are a lot of things to say about how to achieve this. So much so that you will find the contents broken down into 3 articles in order to make it easier to digest:
- Part 1: interface-level parameters, one-parameter functions, const parameters,
- Part 2: calling contexts, strong types, parameters order,
- Part 3: packing parameters, processes, levels of abstraction.
To support this series I’ve taken many examples from interfaces I’ve worked on, except that I’ve stripped out all domain aspects to make them both simpler and disclosable.
Don’t create a parameter that only the function’s implementation understands
It took me a while to formalise this one. I saw there was a pattern behind many interfaces that were difficult for me to use and to read, but for a long time I couldn’t quite put my finger on what it was they did wrong.
The thing is that, when we design a function, it’s common to adjust its interface at the same time as we write its implementation. And this is OK. After all, we can’t always anticipate every aspect of a function we’re designing, and writing its code puts us right in front of its actual requirements.
The problem happens when we add a parameter because we need it to implement the function, but this parameter makes no sense for a caller of the interface.
What does this look like? Consider the following example. This function computes a value based on an input, and maintains an internal cache to retrieve the results it already computed in previous calls (also called memoization). As an additional feature, we want to let the caller choose whether they want the cached value if it exists, or if they always want the value to be actually computed even if it is already in cache (say for a performance measurement for example).
The implementation of the function could look like this:
Output computeValue(Input const& input, bool doCompute) { if (doCompute || !isInCache(input)) { // perform computation // store the result in cache // return it } else { // fetch the result in cache // return it } }
The inside of the function looks logical: the function computes the value if the users asked for this (doCompute
) or if it isn’t in the cache. But look how this interface looks from the outside:
Output computeValue(Input const& input, bool doCompute);
When I read an interface like this, I can almost feel a cloud of question marks floating about my head. I’m wondering: “What should I pass as a doCompute
parameter? I’m calling a function named computeValue
, so of course I want it to compute! So should I pass true
? And what if I pass false
?” This is confusing.
Changing the meaning of a parameter to make it obvious to the client of the interface
Even if it is obvious inside the function implementation, the client of the interface has not been informed that it can force computation and not look into caching. To fix this issue we just need to change the meaning of the parameter:
Output computeValue(Input const& input, bool useCaching) { if (!useCaching || !isInCache(input)) { // perform computation // store the result in cache // return it } else { // fetch the result in cache // return it } }
It still makes sense inside the implementation, and it is also a language that the client of the interface can understand.
Changing the name of the parameter
Sometimes, just making the name of a parameter more explicit is helpful. Let’s see an example: the following function searches for a good programming reference in an book service accessible through the object service
. If the service is not available, the function needs a default book to fall back onto:
Book getGoodProgrammingBook(Service const& service, Book const& book) { if (service.isAvailable()) { // high value algorithm // that determines what makes // a good read for a programmer, // by querying the service. } else { return book; } }
Seen from the outside, this function doesn’t say why to get a book you need to provide a book in the first place, as an argument:
Book getGoodProgrammingBook(Service const& service, Book const& book)
To clarify its intentions, the interface could be more explicit about what its argument is intended for:
Book getGoodProgrammingBook(Service const& service, Book const& bookIfServiceDown);
Pull out the bits that don’t make sense
Another option to deal with the unclear function parameter is to remove it from the function altogether. On the previous example using the book service, we can pull out all the code related to the difficult argument, and move this responsibility over to the caller:
Book getGoodProgrammingBook(Service const& service) { // high value algorithm // that determines what makes // a good read for a programmer, // by querying the service. }
Often this leads to better cohesion in the function: they do only one thing, and do it well. However, applying this technique gets harder when the code is called at multiple places in the codeline, because it leads to duplication. But it can also make the code more natural as a whole, because each context may have its favorite way to react when the service is down.
Whichever way you prefer to fix the code, the guideline I propose is this: every time you define a function parameter, make sure that an uninformed caller would immediately understand what to pass for it, and without seeing the implementation code. If they would wonder about this what to pass for this parameter, redesign.
Consider making one-parameter functions read like English
I owe this piece of advice to my colleague Florent. It comes from the observation that, when a function has only one parameter, there is just a parenthesis that separates the function name from the argument passed:
myFunction(myParameter);
This gives us an opportunity to make the function call look like an English sentence, which I suppose should always be clearer than code (or should it?). To illustrate, consider this function that computes the number of days in a given year:
int numberOfDays(int year); std::cout << "There are " << numberOfDays(2017) << " days in 2017.\n";
Now what if we add the particle “In” in the name of the function?
int numberOfDaysIn(int year); std::cout << "There are " << numberOfDaysIn(2017) << " days in 2017.\n";
It reads a little smoother, doesn’t it?
Note that this is specific to funtions taking one parameter, because the comma separating several arguments makes it harder to write something that looks like English and that feels natural.
If you declare a parameter by value const
, don’t do it in the header file
It is seen as bad practice to modify the value of a parameter inside the implementation of a function:
int f(int x) { ++x; return 2 * x; }
Even though a caller won’t see a difference in such a case (indeed, the parameter passed by value is a copy of the argument the caller passed), this is considered bad practice. The reason is that if a part of a function modifies a parameter, you may miss it when implementing the rest of the function. You would then use an altered value of the parameter where your were thinking to use the original one.
For this reason, some add a const
to the value parameters:
int f(const int x) { ++x; // this no longer compiles return 2 * x; }
I don’t think this is bad, although I don’t do it because it adds redundant information in the prototype of the function. However, whatever your practice there is one thing that you shouldn’t do: don’t show those const
s in the declarations of your functions, typically in a header file. They are merely a help for the implementation.
And you don’t even have to put the const
s in the function declaration. Indeed, the following code compiles and works just like we would expect:
#include <iostream> void f(int); // declaration of f, seen by g - no const void g() { f(42); } void f(const int) // definition of f, with the const { std::cout << "f is called\n"; } int main() { g(); }
Here is what this program outputs:
f is called
So you can omit this particular const
in the function declaration, when they are separate from the function definition. This will make the interface lighter to read, and even more so when there are several parameters.
Stay tuned for the next episode on this series on function parameters! And if you have an opinion about how to make function parameters clarify the intent of an interface, I’d love to hear it.
Don't want to miss out ? Follow:   Share this post!