When we write functions, it’s common to want to give the user options to suit a range of use-cases. There are good ways and bad ways of doing it, and this post is going to explore them.

Guiding principles

API design is hard. A good API needs to be:

  1. Easy to change without breaking user code.
  2. Simple for the common use-cases.
  3. Easy to understand without having to read documentation.
  4. Helpful when things go wrong.

We’ll use these principles to help us understand good and bad design decisions in our case study.

The case study

Let’s imagine we’re writing an HTTP client library in Go.

package http

struct Response {
  StatusCode int
  Headers map[string]string
  Body string
}

func Get(url string) (Response, error) {
  // ...
}

The body of the function isn’t super important. We’re aiming to provide a simple API to the user, and what we’ve got so far is great but it suffers from a lack of flexibility. We can’t set headers, we have no concept of timeouts, it’d be nice to be able to follow redirects easily. In short: we’re missing a lot of vital functionality.

Go: adding parameters the wrong way

Let’s go about this the naive way.

func Get(url string, followRedirects bool, headers map[string]string) (Response, error) {
  // ...
}

This isn’t the most pleasant function to call:

rsp, err := http.Get("http://example.com", false, nil)

If you come across that without knowing the function signature, you’ll be wondering what the false and nil represent. If you have to call the function, you’ll feel resentful having to pass parameters you don’t care about. The common use-case gets more difficult the more parameters you need to pass.

It’s a nightmare to change. Adding or removing parameters will break user code. Adding more functions to achieve the same thing but with different sets of parameters will get confusing.

This is, in short, a mess.

Go: adding parameters a good way

What if we put the parameters in to a struct?

struct Options {
  FollowRedirects bool
  Headers map[string]string
}

func Get(url string, options Options) (Response, error) {
  // ...
}

rsp, err := Get("http://example.com", Options{
  FollowRedirects: true,
  Header: map[string]string{
    "Accept-Encoding": "gzip",
  },
})

Pros

  • Easy to add new parameters.
  • Easy for a casual reader to know what’s going on.
  • User only has to set the parameters they care about.

Cons

  • If you don’t want to set any parameters, you still have to pass in an empty struct. The common use-case is still more difficult than it needs to be.
  • If you want to set lots of parameters, it can get unwieldy.
  • In Go, it’s hard to distinguish between something that’s unset or has been specifically set to a zero-value. Other languages have the same problem with null values.

Go: adding parameters a better way

This one is a little more complex, but bear with it.

type options struct {
  followRedirects bool
  headers map[string]string
}

type Option = func(*options)

var (
  FollowRedirects = func(o *options) {
    o.followRedirects = true
  }

  Header = func(key, value string) func(*options) {
    return func(o *options) {
      o.headers[key] = value
    }
  }
)

func Get(url string, os ...Option) (Response, error) {
  o := options{}
  for _, option := range os {
    option(&o)
  }

  // ...
}

Some examples of using this:

rsp, err := http.Get("http://example.com")

If you want to specify parameters:

rsp, err := http.Get("http://example.com",
  http.FollowRedirects,
  http.Header("Accept-Encoding", "gzip"))

Pros

  • Easy to add new parameters.
  • Easy to deprecate old parameters by outputting a warning when they’re used.
  • Easy for a casual reader to know what’s going on.
  • Functions can do anything, so parameters could go beyond specifying values. For example, you could load configuration from disk.

Cons

  • If the API author isn’t careful, they can create parameters that interfere with each other or do unsafe things like change global state.
  • A little more complicated for the API author to set up than plain old function arguments.

The pros outweigh the cons. The flexibility and clean API surface will pay for themselves, provided you keep check on what you’re doing inside of Option functions.

This is my go-to for optional parameters in languages that don’t have first-class support for them.

“But what about other languages?”

You may be thinking: “but Java has method overloading, wouldn’t that work perfectly for optional parameters?”

It’s a good question, we can get around the need to specify a default Options struct when we don’t want to change anything. Let’s explore how this might look using method overloading in Java.

The case study in Java

Here’s the original case study again, but restated in Java.

public final class Http {
  public static final class Response {
    public final int statusCode;
    public final InputStream body;
    public final Map<String, String> headers;
  }

  public static Response get(String url) throws IOException {
    // ...
  }
}

Calling it would look something like this:

try {
  Response res = Http.get("https://example.com");
} catch (IOException e) {
  // ...
}

Java: the method overloading approach (bad)

Let’s now use method overloading to get around having to specify a default Options struct.

public final class Http {
  public static Response get(String url) throws IOException {
    return get(url, null);
  }

  public static Response get(String url, Map<String, String> headers) throws IOException {
    return get(url, headers, false);
  }

  public static Response get(String url, boolean followRedirects, Map<String, String> headers) throws IOException {
    // ...
  }
}

This is commonly referred to as a “telescopic function,” because as you add new parameters the whole thing gets longer, like extending a telescope.

Here’s an example of using it:

public final class Main {
  public static final void main(String... args) throws IOException {
    Response res;
    res = Http.get("https://example.com");
    res = Http.get("https://example.com", true);
    res = Http.get("https://example.com", true, Map.of("Accept-Encoding", "gzip"));
  }
}

Pros

  • No need for the user to worry about specifying all parameters. The common use-case is simple.
  • Easy to add new parameters.

Cons

  • A lot of boilerplate to set up.
  • While the common use-case is simple, if you want to tweak one parameter you may have to specify loads of other ones you don’t care about.

This isn’t an ideal solution. It improves on the previous, but isn’t what I would recommend you use in your own code.

Java: the Good Solution

public final class Http {
  private static final class Options {
    boolean followRedirects;
    Map<String, String> headers;

    Options() {
      this.followRedirects = false;
      this.headers = new HashMap<>();
    }
  }

  public static Consumer<Options> followRedirects() {
    return o -> o.followRedirects = true;
  }

  public static Consumer<Options> header(String key, String value) {
    return o -> o.headers.put(key, value);
  }

  public static Response get(String url, Consumer<Options>... os) throws IOException {
    Options options = new Options();
    for (Consumer<Options> o : os) {
      o.accept(options);
    }

    // ...
  }
}

And an example of using this API:

public final class Main {
  public static final void main(String... args) throws IOException {
    Response res;
    res = Http.get("https://example.com");
    res = Http.get("https://example.com", Http.followRedirects());
    res = Http.get("https://example.com", Http.followRedirects(), Http.header("Accept-Encoding", "gzip"));
  }
}

This has all of the pros of when we saw it in Go, and still manages to look nice in Java. It also gets around the problem we saw in the previous section of the user that may only want to specify one parameter.

While the above is nice, it’s not what you’re going to see in the wild when using Java. It’s far more likely you’ll see “builders.”

Java: the idiomatic solution using builders

public final class HttpClient {
  private final Map<String, String> headers;
  private final boolean followRedirects;

  private HttpClient(Map<String, String> headers, boolean followRedirects) {
    this.headers = headers;
    this.followRedirects = followRedirects;
  }

  public static final class Builder {
    private Map<String, String> headers = new HashMap<>();
    private boolean followRedirects = false;

    private Builder() {}

    public Builder withHeader(String key, String value) {
      this.headers.put(key, value);
      return this;
    }

    public Builder followRedirects() {
      this.followRedirects = true;
      return this;
    }

    public Client build() {
      return new Client(headers, followRedirects);
    }
  }

  public static Builder builder() {
    return new Builder();
  }

  public Response get(String url) throws IOException {
    // ...
  }
}

And an example of using it:

public final class Main {
  public static final void main(String... args) {
    HttpClient client =
      HttpClient.builder()
        .withHeader("Accept-Encoding", "gzip")
        .followRedirects()
        .build();

    Response res = client.get("https://example.com");
  }
}

This might feel like a bit of a departure from passing in optional parameters to a method. Creating new objects in Java is cheap and preferred over long method signatures.

Pros

  • Can add new parameters without breaking user code.
  • Easy for a casual reader to know what’s going on.
  • Function completion on the builder tells you what parameters are available.

Cons

  • Lots and lots of boilerplate for the library author.

This is the way I would recommend you support optional parameters in Java. It may feel like a lot of work, but there are libraries that can help reduce the boilerplate. Also IDEs like IntelliJ have tools to help you generate most of the boring stuff.

Why is this more idiomatic than the other method?

Builders predate lambdas in Java.

“But what about languages that support optional parameters?”

Another great question. In these cases, it makes the most sense to use what the language offers you.

Python

class Http:
  @staticmethod
  def get(url, follow_redirects=False, headers=None):
    pass

res = Http.get("https://example.com")
res = Http.get("https://example.com", follow_redirects=True)
res = Http.get("https://example.com", headers={"Accept-Encoding": "gzip"})

Ruby

class Http
  def self.get(string, follow_redirects: false, headers: nil)
  end
end

res = Http.get("https://example.com")
res = Http.get("https://example.com", headers: {"Accept-Encoding": "gzip"})
res = Http.get("https://example.com", follow_redirects: true, headers: {"Accept-Encoding": "gzip"})

Pros

  • Easy to add new parameters.
  • Easy for a casual reader to know what’s going on.
  • It has little to no boilerplate for the API author.

Cons

  • Some oddities around setting default parameter values that we’ll touch on later.

Cautionary words for dynamic languages

Beware **kwargs

Both Ruby and Python have the ability to “glob” their named arguments in to dictionaries:

class Http
  def self.get(string, **kwargs)
    # kwargs[:headers]
    # kwargs[:follow_redirects]
  end
end

res = Http.get("https://example.com")
res = Http.get("https://example.com", headers: {"Accept-Encoding": "gzip"})
res = Http.get("https://example.com", follow_redirects: true, headers: {"Accept-Encoding": "gzip"})
class Http:
  @staticmethod
  def get(url, **kwargs):
    # kwargs["headers"]
    # kwargs["follow_redirects"]
    pass

res = Http.get("https://example.com")
res = Http.get("https://example.com", follow_redirects=True)
res = Http.get("https://example.com", headers={"Accept-Encoding": "gzip"})

I would recommend against using these. Being explicit makes it more clear to the reader what they can pass in, and it also gives you the author an opportunity to set sensible defaults.

Beware mutable default values

You may have wanted to write this a few sections ago:

class Http:
  @staticmethod
  def get(url, follow_redirects=False, headers={}):
    pass

Note the difference in headers={} from above, where we wrote headers=None in Python and headers=nil in Ruby.

The problem with this in Python is that the empty dictionary isn’t created every time the method gets called. It’s created once when the class is defined, and so is shared between invocations. This isn’t true in Ruby.

Here’s an example:

class Http:
  @staticmethod
  def get(url, follow_redirects=False, headers={}):
    if "counter" not in headers:
        headers["counter"] = 0
    headers["counter"] += 1
    print(headers)

Http.get("https://example.com")
Http.get("https://example.com", headers={"counter": 100})
Http.get("https://example.com")

This outputs:

{'counter': 1}
{'counter': 101}
{'counter': 2}

Equivalent in Ruby:

class Http
  def self.get(url, follow_redirects: false, headers: {})
    headers[:counter] ||= 0
    headers[:counter] += 1
    puts headers
  end
end

Http.get("https://example.com")
Http.get("https://example.com", headers: { counter: 100 })
Http.get("https://example.com")

Outputs:

{:counter=>1}
{:counter=>101}
{:counter=>1}

Even though the problem isn’t present in Ruby, I like to avoid it regardless.

Conclusion

I hope this post has given you some food for thought, and shown you some nice techniques you hadn’t considered and why they’re nice.

If you know other methods to achieve this goal that I haven’t explored here, I’d love to read about it.