The builder and parameter bundle patterns allow for giving a class a lot of configuration options at compile time, while leaving the runtime API small and the runtime constraints clear.
Suppose that you are implementing a new class C, which has a name, a color, a size, and a URL associated with it, along with a handful of boolean parameters, each of which default to false. All of these values should be set before the C is actually used for anything. That class might look like this:
class C { public: // Invariant: none of the members can change after you call Init(). void Init(); private: const std::string name_; const gfx::Color color_; const gfx::Size size_; const GURL url_; const bool is_cool_ = false; const bool is_nice_ = false; const bool is_friendly = false; };
There are three ways you might choose to make this class configurable:
Most classes end up doing one of these, and have an API that looks roughly like this:
class C { public: C(std::string name, gfx::Color color, gfx::Size size, GURL url); void Init(); void set_is_cool(bool is_cool) { is_cool_ = is_cool; } void set_is_nice(bool is_nice) { is_nice_ = is_nice; } void set_is_friendly(bool is_friendly) { is_friendly_ = is_friendly; } private: ... // Note that these now have to be non-const, even though they are // logically const, because the setters are exposed to mutate them before // use. bool is_cool_; bool is_nice_; bool is_friendly_; };
Your class is forced into either:
The builder pattern (and the parameter bundle pattern, a simpler relative) offer a way out of this bind.
A parameter bundle is an object that encapsulates all the parameters to another method, often to a constructor. For example, we might structure our class C like this:
class C { public: struct Params { std::string name; // ... }; C(Params params); private: const std::string name_; // ... };
As long as the params struct is well-structured this may be all you need, and it can often significantly simplify constructing complicated classes if callers are allowed to fill in only those parts of the param bundle that they care about.
However, there's a problem: constructors are not allowed to fail in C++. That implies that it has to be impossible to make an invalid C::Params
, because if we did pass such an invalid parameter bundle into C::C()
, the constructor would have no way to signal failure. We really want to ensure that we can only get an instance of C
that is configured in a valid way.
We can do that by adding a factory function to C:
class C { public: unique_ptr<C> Make(Params params); private: // Since this is private, we can assume Params was validated by the // factory function first. C(Params params); };
But in the process, we've cluttered the public API of C up with a lot of the details about how one manufactures an instance of C. What if we could avoid doing that? Enter...
The key idea here is that instead of defining a C::Params
class, we define a C::Builder
class, which knows how to configure and then construct a new instance of C
. That might look like this:
class C { public: class Builder; // defined elsewhere private: friend class Builder; C(Builder builder); // only called from C::Builder };
and now our Builder
class has an interface like this:
class Builder { public: Builder(); void set_name(std::string name); void set_color(gfx::Color color): // ... std::unique_ptr<C> Build(); }
To an extent, this is just a syntactic variation of the parameter bundle pattern, but it does allow for one really nice convenience technique. If, instead of returning void, we have our setters return a reference to the Builder object itself:
class Builder { public: Builder(); Builder& set_name(std::string name); Builder& set_color(gfx::Color color); // ... std::unique_ptr<C> Build(); }
then instead of writing this:
C::Builder builder; builder.set_name(name); builder.set_color(color); auto c = builder.Build();
we can write this:
auto c = C::Builder().set_name(name) .set_color(color) .Build();
which lets us avoid naming the temporary builder object.
It's also possible to use a version of the parameter/builder pattern only for optional arguments. In this case we make use of Params&
to be able to chain calls like Params().set_foo().set_bar()
, but we may not have a Build() method.
In the first example, this would be:
class C { public: class Params { public: Params(); Params& set_is_cool(bool is_cool) { is_cool_ = is_cool; return *this; } ... bool is_cool() const { return is_cool_; } ... private: bool is_cool_ = false; bool is_nice_ = false; bool is_friendly = false; }; // Populates all fields. Optional fields take their values from `params`. C(std::string name, gfx::Color color, gfx::Size size, GURL url, Params params = Params()); private: const std::string name_; const gfx::Color color_; const gfx::Size size_; const GURL url_; const bool is_cool_; const bool is_nice_; const bool is_friendly; };
Doing so lets us construct an object without awareness of the optional Params
argument:
C("foo", ...)
While making C
objects with optional parameters readable and without temporary variables:
C("foo", ..., Params().set_is_cool(true));