{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/dysfunctional-options-pattern/",
"description": "Discover a simpler alternative to functional options: method chaining with builder-style configuration that's 76x faster and easier to understand.",
"path": "/go/dysfunctional-options-pattern/",
"publishedAt": "2024-03-06T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"API",
"Performance"
],
"textContent": "Ever since Rob Pike published the text on the [functional options pattern], there's been no\nshortage of blogs, talks, or comments on how it improves or obfuscates configuration\nergonomics.\n\nWhile the necessity of such a pattern is quite evident in a language that lacks default\narguments in functions, more often than not, it needlessly complicates things. The situation\ngets worse if you have to maintain a public API where multiple configurations are controlled\nin this manner.\n\nHowever, the pattern solves a valid problem and definitely has its place. Otherwise, it\nwouldn't have been picked up by pretty much every other library, whether it's [Ngrok] or the\n[Elasticsearch agent].\n\nIf you have no idea what I'm talking about, you might want to give my previous write-up on\n[configuring options] a read.\n\nFunctional options pattern\n\nAs a recap, here's how the functional options pattern works. Let's say, you need to allow\nthe users of your API to configure something. You can expose a struct from your package\nthat'll be passed to some other function to tune its behavior. For example:\n\nThen the Config struct will be imported, initialized, and passed to the Do function by\nyour API users:\n\nThis is one way of doing that, but it's generally discouraged since it requires you to\nexpose the internals of your API to the users. So instead, a library usually exposes a\nfactory function that'll do the struct initialization while keeping the struct and fields\nprivate. For instance:\n\nThe API consumers will now use NewConfig to produce a configuration and then pass the\nstruct instance to the Do function as follows:\n\nThis approach is better as it keeps the internal machinery hidden from users. However, it\ndoesn't allow for setting default values for some configuration attributes; all must be set\nexplicitly. What if your users want to override the values of multiple attributes? This\nleads your configuration struct to be overloaded with options, making the NewConfig\nfunction demands numerous positional arguments.\n\nThis setup isn't user-friendly, as it forces API users to explicitly pass all these options\nto the NewConfig factory. Ideally, you'd initialize config with some default values,\noffering users a chance to override them. But, Go doesn't support default values for\nfunction arguments, which compels us to be creative and come up with different workarounds.\nFunctional options pattern is one of them.\n\nHere's how you can build your API to leverage the pattern:\n\nThen you'd use it as follows:\n\nFunctional options pattern relies on functions that modify the configuration struct's state.\nThese modifier functions, or option functions, are defined to accept a pointer to the\nconfiguration struct config and then directly alter its fields. This direct manipulation\nis possible because the option functions are closures, which means they capture and modify\nthe variables from their enclosing scope, in this case, the config instance.\n\nIn the NewConfig factory, the variadic parameter opts ...option allows for an arbitrary\nnumber of option functions to be passed. Here, opts represents the optional configurations\nthat the users can override if they want to.\n\nThe NewConfig function iterates over this slice of option functions, invoking each one\nwith the &c argument, which is a pointer to the newly created config instance. The\nconfig instance is created with default values, and the users can use the With functions\nto override them.\n\nCurse of indirection\n\nThat's a fair bit of indirection just to allow API users to configure some options. I don't\nknow about you, but multi-layered higher-order functions hurt my brain. It's quite slow as\nwell.\n\nAll this complexity could have been avoided if Go allowed default arguments in functions.\nYour configuration factory could simply grab the default values from the keyword arguments\nand pass them to the underlying struct. The idea that supporting default arguments in\nfunctions would lead to a parameter explosion seems unfounded, especially when the\nalternative requires gymnastics like the functional option pattern.\n\nAlso, the multiple layers of indirection hinder API discoverability. Trying to discover\nmodifier functions by hovering your cursor over the factory function's return value in the\nIDE won't be very helpful, as these functions are defined at the package level.\n\nSo, if you need to configure multiple structs in this manner, the explosion of their\nrespective package-level modifiers make it even harder for the user to know which function\nthey'll need to use to update a certain configuration attribute.\n\nRecently, I've spontaneously stumbled upon a fluent-style API to manage configurations that\ndon't require so many layers of indirection and lets you expose optional configuration\nattributes. Let's call it dysfunctional options pattern.\n\nDysfunctional options pattern\n\nThe idea is quite similar to how the API with functional options pattern is constructed.\nHere's the complete implementation:\n\nYou'd use the API as follows:\n\nSimilar to the previous pattern, we have modifiers here too. However, instead of being\nhigher order functions, the modifiers are methods on config and return a pointer to the\nstruct.\n\nThe NewConfig factory function instantiates the config struct with some default values\nand returns the struct pointer like the modifiers. This enables us to chain the WithFizz\nand WithBazz modifiers on the returned value of NewConfig and update the values of the\noptional configuration attributes.\n\nApart from simplicity and the lack of magic, you can hover over the return type of the\nfactory and immediately know about the supported modifier methods.\n\nI did a [rudimentary benchmark] of the two approaches and was surprised that the second one\nwas roughly ~76x faster on Go 1.22!\n\nHere's an example in [fork-sweeper's CLI code].\n\n_P.S. This is indeed a lightweight spin on what OO languages call the builder pattern.\nHowever, I didn't call it that because there's no mandatory .Build() method to be called\nat the end of the method chain._\n\n\n\n\n[functional options pattern]:\n https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html\n\n\n[ngrok]:\n https://github.com/ngrok/ngrok-api-go/blob/ec1a3e91cae94c70f0e5c31b95aed5a1d6dd65b7/client_config.go\n\n\n[elasticsearch agent]:\n https://github.com/elastic/elastic-agent/blob/4aeba5b3fcf0d72924c70ff2127996a817b83a23/pkg/testing/fetcher_http.go\n\n\n[configuring options]:\n /go/configure-options/\n\n\n[rudimentary benchmark]:\n https://gist.github.com/rednafi/08fe371ed31072ab0bd96bf51611660a\n\n[fork-sweeper's CLI code]:\n https://github.com/rednafi/fork-sweeper/blob/80e1f7c76a2efcb7d1b65d6b12303c590bb74c2c/src/cli.go#L172",
"title": "Dysfunctional options pattern in Go"
}