If for example I have a builder set up so I can create objects like so:
Node node = NodeBuilder()
.withName(someName)
.withDescription(someDesc)
.withData(someData)
.build();
How can I make sure that all variables used to build the object have been set before the build method?
Eg:
Node node = NodeBuilder()
.withName(someName)
.build();
Isn't a useful node because the description and data haven't been set.
The reason I'm using the builder pattern is because without it, I'd need a lot of combination of constructors. For example the name and description can be set by taking a Field
object, and the data can be set using a filename:
Node node = NodeBuilder()
.withField(someField) //Sets name and description
.withData(someData) //or withFile(filename)
.build(); //can be built as all variables are set
Otherwise 4 constructors would be needed (Field, Data), (Field, Filename), (Name, Description, Data), (Name, Description, Filename). Which gets much worse when more parameters are needed.
The reason for these "convenience" methods, is because multiple nodes have to be built, so it saves a lot of repeated lines like:
Node(modelField.name, modelField.description, Data(modelFile)),
Node(dateField.name, dateField.description, Data(dateFile)),
//etc
But there are some cases when a node needs to be built with data that isn't from a file, and/or the name and description are not based on a field. Also there may be multiple nodes that share the same values, so instead of:
Node(modelField, modelFilename, AlignLeft),
Node(dateField, someData, AlignLeft),
//Node(..., AlignLeft) etc
You can have:
LeftNode = NodeBuilder().with(AlignLeft);
LeftNode.withField(modelField).withFile(modelFilename).build(),
LeftNode.withField(dateField).withData(someData).build()
So I think my needs match the builder pattern pretty well, except for the ability to build incomplete objects. The normal recommendation of "put required parameters in the constructor and have the builder methods for the optional parameters" doesn't apply here for the reasons above.
The actual question: How can I make sure all the parameters have been set before build is called at compile time? I'm using C++11.
(At runtime I can just set a flag bits for each parameter and assert that all the flags are set in build)
Alternatively is there some other pattern to deal with a large number of combinations of constructors?
In the builder: A build() method which calls the method, passing in each field. It returns the same type that the target returns. In the builder: A sensible toString() implementation. In the class containing the target: A builder() method, which creates a new instance of the builder.
Builder design pattern is a creational design pattern like Factory Pattern and Abstract Factory Pattern.
Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.
Disclaimer: This is just a quick shot, but I hope it gets you an idea of what you need.
If you want this to be a compiler time error, the compiler needs to know about the currently set parameters at every stage of the construction. You can achieve this by having a distinct type for every combination of currently set parameters.
template <unsigned CurrentSet>
class NodeBuilderTemplate
This makes the set parameters a part of the NodeBuilder
type; CurrentSet
is used as a bit field. Now you need a bit for every available parameter:
enum
{
Description = (1 << 0),
Name = (1 << 1),
Value = (1 << 2)
};
You start with a NodeBuilder that has no parameters set:
typedef NodeBuilderTemplate<0> NodeBuilder;
And every setter has to return a new NodeBuilder with the respective bit added to the bitfield:
NodeBuilderTemplate<CurrentSet | BuildBits::Description> withDescription(std::string description)
{
NodeBuilderTemplate nextBuilder = *this;
nextBuilder.m_description = std::move(description);
return nextBuilder;
}
Now you can use a static_assert
in your build
function to make sure CurrentSet
shows a valid combination of set parameters:
Node build()
{
static_assert(
((CurrentSet & (BuildBits::Description | BuildBits::Name)) == (BuildBits::Description | BuildBits::Name)) ||
(CurrentSet & BuildBits::Value),
"build is not allowed yet"
);
// build a node
}
This will trigger a compile time error whenever someone tries to call build()
on a NodeBuilder
that is missing some parameters.
Running example: http://coliru.stacked-crooked.com/a/8ea8eeb7c359afc5
I ended up using templates to return different types and only have the build method on the final type. However it does make copies every time you set a parameter:
(using the code from Horstling, but modified to how I did it)
template<int flags = 0>
class NodeBuilder {
template<int anyflags>
friend class NodeBuilder;
enum Flags {
Description,
Name,
Value,
TotalFlags
};
public:
template<int anyflags>
NodeBuilder(const NodeBuilder<anyflags>& cpy) : m_buildingNode(cpy.m_buildingNode) {};
template<int pos>
using NextBuilder = NodeBuilder<flags | (1 << pos)>;
//The && at the end is import so you can't do b.withDescription() where b is a lvalue.
NextBuilder<Description> withDescription( string desc ) && {
m_buildingNode.description = desc;
return *this;
}
//other with* functions etc...
//needed so that if you store an incomplete builder in a variable,
//you can easily create a copy of it. This isn't really a problem
//unless you have optional values
NodeBuilder<flags> operator()() & {
return NodeBuilder<flags>(*this);
}
//Implicit cast from node builder to node, but only when building is complete
operator typename std::conditional<flags == (1 << TotalFlags) - 1, Node, void>::type() {
return m_buildingNode;
}
private:
Node m_buildingNode;
};
So for example:
NodeBuilder BaseNodeBuilder = NodeBuilder().withDescription(" hello world");
Node n1 = BaseNodeBuilder().withName("Foo"); //won't compile
Node n2 = BaseNodeBuilder().withValue("Bar").withName("Bob"); //will compile
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With