Skip to content

How to design, or review, an API

Laurent Rineau edited this page Dec 1, 2020 · 8 revisions

Table of Contents

I want to write my ideas I have about the review and the design of an API (application programming interface). The design of an API should tend to promote interfaces that are easy to review and to understand. That is not only important for people trying to understand or review your code, but also for yourself, when you will try to understand and maintain your own code, a few weeks or months after it was first written.

Functions

A function takes inputs as parameters and returns a result.

Quoted from the C++ Core Guidelines:

It should be possible to name a function meaningfully, to specify the requirements of its argument, and clearly state the relationship between the arguments and the result.

Inputs, outputs, and side-effects

During a review of code, we need to identify from the signature of the function what are the inputs, and the outputs. A function may also have side-effects, like modifying global variables or data members of an object.

Categories of parameters

There are three categories of parameters:

  • input parameters, that must be set by the caller of a function, and will be read by the body of the functions,
  • output parameters, that maybe be left uninitialized, or empty, by the caller, and the body of the function set or fill them to pass back information to the caller, and
  • input/output parameters, that will be read by the body of the function, and then set to pass back information to the caller.

In C++, there are various ways to implement these categories but mainly:

  • input parameters are passed by value, or by reference to const,
  • output and input/output parameters are passed by reference to non-const.
✋ Notes
In C++ output and input/output parameters are passed the same way. They cannot be distinguished without a documentation or a look at the implementation of the function. That is why input/output parameters should be avoided.
Output parameters should also be avoided: prefer return values.

Member functions

Member functions of a class implicitly take *this as the implicit object argument. In const-qualified member functions, *this is const as well, meaning that *this is an input of the function. In non-const member functions, *this is an input/output.

Exception: member functions prefixed with static are static member functions. They do not have the implicit object argument.

Pure functions

A function without output parameter and no side-effects is named a "pure function". Pure functions should be preferred because they are easier to reason about.

Review of a function signature

While reading the code, the return type and the type of the arguments of functions or member functions should be sufficient to understand what are the inputs and outputs of a function. That means functions with side-effects should be limited to non-const member functions with clear and documented semantic.

Recommendations

  • By default, make member functions const.
  • Qualify member functions with static if they do not read or modify the object, when possible.
  • Avoid side-effects: it should be limited to non-const member functions with a clear and documented semantic.
  • Avoid input/output parameters, so that the reader of the code can assume any reference to non-const is an output parameters.
  • Avoid output parameters, and prefer a return value.

Review functions bodies, and split into smaller functions

The first three rules about functions, in the C++ Core Guidelines are:

During a frenetic prototyping phase, we are very frequent quite far from following those three rules.

Name of a function

If a function is carefully named, and its inputs and outputs are clearly defined by the signature, then the documentation is at least redundant and maybe useless. We should always try to reach that sort of clarity.

Keep functions short and simple

A function should have a well defined behavior, and should be kept short. If the function body does not fit in one screen, then you should consider refactor its body into small functions with well defined behaviors (well defined input/outputs, and an explicit name).

Avoid copy-pasting inside the body of a function

If several ranges of code were written using copy-paste-modify, then they are good candidates for a function. Identify what differs between the different versions of the copy-pasted piece of code. Those differences will probably define the inputs and outputs of the function to create. That function does not need to be part of the API of the code:

  • If functions definitions are in a .cpp file different from the header that defines them, then the new function can be a new function defined in the .cpp file, but not declared in the header.
  • It can also be a lambda-expression (an anonymous function object) that is defined in the bigger function and then used several times in it.
  • It can be a private function of a class.

How to split a functions into smaller ones

A function with a large body is certainly composed of blocks of code. Identify loops bodies, blocks in if-statements. You may also identify consecutive lines of code that share a purpose (like the initialization of a complicated data-structure). Given those blocks:

  • identify what are the inputs and outputs of the block,
  • the wanted behavior of the block.
  • Try to name the block.

Then the block is ready to become a function.