Jonathan.
Jonathan.

Reputation: 55594

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?

Upvotes: 15

Views: 2389

Answers (5)

Sklert
Sklert

Reputation: 242

This question can not be outdated. Let me share my solution to this problem.

class Car; //object of this class should be constructed

struct CarParams{
protected:
    std::string name_;
    std::string model_;
    int numWheels_;
    int color_;

    struct Setter_model;
    struct Setter_numWheels;
    struct Setter_color;

public:    
    class Builder;
};

struct CarBuilder : CarParams{ //starts the construction
    Setter_model& set_name(const std::string& name){
        name_ = name;
        return reinterpret_cast<Setter_model&>(*this);
    }
};

struct CarParams::Setter_model : CarParams{
    Setter_numWheels& set_model(const std::string& model){
        model_ = model;
        return reinterpret_cast<Setter_numWheels&>(*this);
    }
};

struct CarParams::Setter_numWheels : CarParams{
    Setter_color& set_numWheels(int numWheels){
        numWheels_ = numWheels;
        return reinterpret_cast<Setter_color&>(*this);
    }
};

struct CarParams::Setter_color : CarParams{
    Builder& set_color(int color){
        color_ = color;
        return reinterpret_cast<Builder&>(*this);
    }
};

class CarParams::Builder : CarParams{
private:
    //private functions
public:
    Car* build();
    // optional parameters

};

The class Car is defined bellow:

class Car{
private:
    std::string name_;
    std::string model_;
    int numWheels_;
    int color_;

public:
    friend class CarParams::Builder;
    //other functions
};

And build function in .cpp:

Car* CarParams::Builder::build(){
    Car* obj = new Car;
    obj->name_ = std::move(name_);
    obj->model_ = std::move(model_);
    obj->numWheels_ = numWheels_;
    obj->color_ = color_;
    return obj;
}

Maybe it is a little bit complicated, but looks nice on client side:

  std::string name = "Name";
  std::string model = "Model";

  Car* newCar = CarBuilder()
                .set_name(name)
                .set_model(model)
                .set_numWheels(3)
                .set_color(0x00ffffff)
                .build();

The error will occur in compile-time, if you miss something before build(). One more disadvantage is the strict order of arguments. It can be combined with optional parameters.

Upvotes: 0

Jonathan.
Jonathan.

Reputation: 55594

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

Upvotes: 2

Horstling
Horstling

Reputation: 2151

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

Upvotes: 9

Serge Ballesta
Serge Ballesta

Reputation: 149065

The only way I can imagine would be to have a number of static builder methods (or constructors) one for each set of required parameters that would return a builder instance, and then simple instance methods to set (or overwrite) parameters and that return the instance.

It will allow compile time checking, but at the price of a much more complex API, so I strongly advise you not to use it unless you really have good reasons to do.

Upvotes: 0

Drop
Drop

Reputation: 13003

Disclaimer: this is an idea. I'm not sure it even works. Just sharing.

You might try to:

  • remove build() method from NodeBuilder
  • regroup your mandatory fields into a single builder method of NodeBuilder, say NodeBuilder::withFieldData(bla, bli, blu) and/or NodeBuilder::withFieldData(structBliBlaBLU).
  • make withFieldData() to return a builder of a different type, say NodeBuilderFinal. Only this type of builder has build() method. You may inherit non-mandatory methods from NodeBuilder. (Strictly speaking, NodeBuilderFinal is a "Proxy" object)

This will enforce user to call withFieldData() before build(), while allowing to call other builder methods in arbitrary order. Any attempt to call build() on non-final builder will trigger compiler error. build() method will not show up in autocompletion until final builder is made ;).

If you don't want monolithic withFieldData method, you may return different proxies from each "field" method, like NodeBuilderWithName, NodeBuilderWithFile, and from those, you can return NodeBuilderWithNameAndFile, etc. until final builder will be built. This is quite hairy and will require many classes to be introduced to cover different orders of "field" calls. Similarly to what @ClaasBontus proposed in comments, you can probably generalize and simplify this with templates.

In theory, you may try to enforce more sophisticated constraints by introducing more proxy objects into the chain.

Upvotes: 0

Related Questions