This chapter discusses considerations about API consistency and provides the following recommendations:
A consistent and well-documented API is crucial for a good development experience. The same is valid for argument order, overall naming scheme, and overloads. Also, it's worth documenting all conventions.
For example, if one of your methods accepts
length as parameters, then so should other methods, instead of, for example, accepting
endIndex. Parameters like these are most likely of
Long type, and thus it's very easy to confuse them.
The same works for parameter order: Keep it consistent between methods and overloads. Otherwise, users of your library might incorrectly guess the order they should pass arguments in.
Here is an example that preserves the parameter order and uses consistent naming:
If you have many lookalike methods, name them consistently and predictably. This is how the
stdlib API works: There are methods
singleOrNull(), and so on. It's clear from their names that they are all pairs, and some of them might return
null while others might throw an exception.
Use a builder DSL
"Builder" is a well-known pattern in development. It allows you to build a complex entity not in a single expression, but gradually while getting more information. When you need to use a builder, it's better to write it using a builder DSL, which is binary-compatible and more idiomatic.
A canonical example of a Kotlin builder DSL is
kotlinx.html. Consider the following example:
It could be implemented as a traditional builder, but that would be considerably more verbose:
This implementation has too many details that you don't necessarily need to know, and it requires you to build each entity at the end.
The situation gets even worse if you need to generate a builder's content dynamically in a loop. In this scenario, you have to instantiate a variable and dynamically overwrite it:
Inside the builder DSL, you can directly call a loop and all necessary DSL calls:
Keep in mind that inside curly braces it's impossible to check at compile time whether you have set all the required attributes. To avoid this, pass required arguments as function arguments, not as builder's properties. For example, if you want
href to be a mandatory HTML attribute, your function will look like:
And not just:
Use constructor-like functions where applicable
Sometimes, you can simplify your API's appearance by using constructor-like functions. A constructor-like function is a function whose name starts with a capital letter, so it looks like a constructor. This approach can make your library easier to understand.
Suppose you want to introduce an Option type in your library:
You can define implementations of all the
Option interface methods –
flatMap(), and so on. However, each time your API users create such an
Option, they must write extra logic to check what they create. For example:
To save your users from having to write the same check each time, you can add just one line to your API:
Now, creating a valid
Option is as simple as can be: Just call
Option(x) and you have a null-safe, purely functional Option idiom.
Another use case for using a constructor-like function is when you need to return "hidden" things, such as a private instance or an internal object. For example, let's look at a method from the standard library:
In the code above,
emptyList() returns the following:
You can write a constructor-like function to lower the cognitive complexity of your code and reduce the size of your API:
Use member and extension functions appropriately
For example, consider the following class for a graph:
This class contains a bare minimum of vertices and edges as private variables, functions to add vertices and edges, and accessor functions that return an immutable representation of the current state.
You can add all the remaining functionality outside the class:
Only properties, overrides and accessors should be members.
Avoid using Boolean arguments in functions
Ideally, a reader should be able to tell the purpose of a function argument just by reading code. With
Boolean arguments, however, this is almost impossible to do, especially if you're not using an IDE (for example, if you're reviewing the code in a version control service). Using named arguments can help clarify the purpose of arguments, but for now there is no way to force developers to use them in IDEs. Another option is to create a function that contains the action of the
Boolean argument and give this function a descriptive name.
For example, in the standard library there are two functions for
It was possible to add something like
map(filterNulls: Boolean) and write code like this:
From reading this code, it's difficult to infer what
false refers to. However, if you use the
mapNotNull() function, readers will be able to understand the logic at a glance:
Learn about APIs':