How to Make the Options Pattern Better

August 1, 2023
How to Make the Options Pattern Better

In our previous post we implemented and talked about benefits of utilising the options pattern to enhance the API of a library we are developing. We explored how this powerful technique can improve user friendliness, flexibility and maintainability.

Today, we are working again on the XYZ company client. We will go through additional strategies that can further improve the way we leverage options pattern in our codebase.

Preventing modification

As a quick recap of the previous post, let’s take a look at how the implementation of client and option looks right now.

    
  

Client ’s attributes are not exported and there’s also a constructor. There’s a reason for all of that. Any unexpected modification can put the client into an invalid state or lead to an unexpected behaviour. The biggest flaw of the basic implementation is that it doesn’t grant immutability when it’s desired . Users are able to update previously created client with a simple trick.

    
  
    
  

All it takes is to pass client to the option function. Our goal is to protect the configuration options by default and allow updates only when it’s safe to do so. How do we achieve that?

Introducing Option interface

There’s another way of defining the option. Instead of the function type we’ll use an interface. Function which operates on the Client and modifies it will now become a method signature.

    
  

The constructor will now call the apply method on the option not the option itself.

    
  

Implementing options

The new Option interface can be implemented in different ways. You might not need all of them, but it’s good to have an extra flexibility. Let’s go through some of the popular ones.

The standard way is done by creating a specific structure for the option and implementing the apply method.

    
  

This approach is very intuitive and simple. However, it generates lots of boilerplate. We can make it more generic so the code can be reused.

    
  

Each option uses the same type. Looking closely at this, notice that this struct is only needed to hold func(*Client) .

A small change can lead us to another (and my personal favourite) implementation.

    
  

You might recognise this approach from the net/http package and its HandlerFunc . It’s more concise while having an almost identical usage.

Similarly, you can do the same with any other type you define. However, it brings us really close to the first implementation.

    
  

You might notice that each example uses extra functions which return Option . When using more generic approaches it adds proper naming for the options and let’s us add comment documentation for each of them. For others, you might be tempted to skip those functions.

    
  

It works exactly the same as the previous one. It required less lines of code. However, it comes at a huge cost - it impaired the documentation.

    
  

This a good example of how some code duplication can be more valuable than simplification when working on API development. The latter usually comes with some tradeoffs that you need to be mindful about.

Options discoverability

Speaking about documentation, let’s take a look how it changed after switching from function type to an interface.

    
  

The change is minimal. The only difference is in the type of our option, everything else remains exactly the same.

As apply function is not exported, we are not exposing the type which option operates on. It removes the temptation to try implementing the options by users. On top of that, it makes any future changes to our code easier. For example, if the Client starts to have a big number of configurable arguments you might want to encapsulate them in a separate structure. Such change would not affect the users in any way. The API and docs would stay exactly the same.

Validating provided options

Many times, provided options must follow a set of rules. In such cases your best bet is adding validation. It ensures correctness of the created structure and informs user about what specifically resulted in a failure.

    
  

Such implementation will return the very first encountered error. If there are more options with invalid values, the user will need to fix them one by one.

As an alternative, we can gather all errors and return them as one message. You can either write the whole logic for that yourself or use an existing library, like go-multierror .

    
  

If user provides more than one option for which the validation fails, the output will look similarly to this:

    
  

ℹ️  Formatting the output If you don’t like how the error looks, don’t worry! You can easily customise the way error is stringified. This is just the default one.

Options list and presets

Sometimes, users might want to create a list of options. The popular use cases are:

  • Predefining a common options for a repeated task
  • Building options based on specific condition

Those tasks seem trivial. After all, all of them can be solved using only a slice.

    
  

But, what if you want to use those options together with other ones in the constructor? The following code will not even compile.

    
  

While this use case might seem a little stretched out, I would not say it’s impossible.

A slice is quite limited in its functionality. This is why you might want to consider adding a new option which would allow representing multiple options as one.

    
  

This solves the mentioned issue. Arguably, it also makes building options more friendly and readable.

Predefining common options, or so called presets, isn’t limited to the users of our code. While this applies mostly to internal libraries, you might want to export some presets if there are common combinations of options for your structure.

    
  

Final words

Hopefully, I have successfully showcased the flexibility and usefulness of the options pattern. Its adaptability allows it to be applied to various use cases and customised to fit your specific needs.

I encourage you to experiment, modify and extend the ideas presented here. Dive deeper into the world of options pattern and be creative. Happy coding! 💻

GoDeveloper Experience