I like builders and have written APIs that provide builder patterns, but I really prefer option maps where the language makes it possible. Instead of a builder like
Wizard wiz = new WizardBuilder("some string")
.withPriority(1)
.withMode(SOME_ENUM)
.enableFoo()
.disableBar()
.build();
I prefer writing something like
Wizard wiz = new Wizard("some string",
{:priority 1
:mode SOME_ENUM
:foo? true
:bar? false})
Why?
- Option maps are usually shorter in languages with map literals.
- Option maps are data structures, not code. They’re easier to store and read from files. You can put them in databases or exchange them across the network. Over and over again I see boilerplate code that sucks in JSON and calls a builder fun for each key. This is silly.
- Builders in most languages (perhaps not Rust!) require an explicit freeze/build operation because they’re, well, mutable. Or you let people clobber them whenever, I guess. :-/
- Option maps compose better. You can write functions that transform the map, or add default values, etc, and call a downstream function. Composing builders requires yielding the builder back to the caller via a continuation, block, fun, etc.
- Option maps are obviously order-independent; builder APIs are explicitly mutating the builder, which means the order of options can matter. This makes composition in builders less reliable.
Why not use option maps everywhere? I suspect it has to do with type systems. Most languages only have unityped maps (e.g. java.util.Map<String, Object>
) where any key is allowed, but options usually have fixed names and specific but heterogenous types. The option map above has booleans, integers, and enums, for example.
In languages like Java, it’s impossible to specify type constraints like “This map has a :foo? key which must be a boolean, and has a :mode key that can only be one of these three values”. Using a builder with explicit type signatures for each function lets you statically verify that the caller is using the correct keys and providing values of the appropriate type. [1]
Of course, all this goes out the window when folks start reading config files at runtime, because you can’t statically verify the config file, so type errors will appear at runtime anyway–but you can certainly get some static benefit wherever the configuration is directly embedded in the code.
[1] Know what a typed heterogenous map is in Java? It’s an Object! From this perspective, builders are just really verbose option maps with static types.
It’s possibly also worth noting that you can always use a builder-style API to create a config map if that’s what you desire.