Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Builder pattern: making sure the object is fully built

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?

like image 507
Jonathan. Avatar asked May 20 '16 10:05

Jonathan.


People also ask

What does builder () build () do?

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.

Which type of design pattern is builder pattern?

Builder design pattern is a creational design pattern like Factory Pattern and Abstract Factory Pattern.

What design pattern lets you preserve the construction steps while allowing sub classes to produce their own types or representation of an object using the same steps?

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.


2 Answers

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

like image 113
Horstling Avatar answered Oct 30 '22 05:10

Horstling


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
like image 29
Jonathan. Avatar answered Oct 30 '22 05:10

Jonathan.