Builder pattern: making sure the object is fully built
Asked Answered
F

5

15

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?

Flyleaf answered 20/5, 2016 at 10:16 Comment(10)
The flag way is probably the easiest way, and then have the build function throw an exception if a mandatory field is not set. I can't think of any way to make it a compile-time check.Blindfish
Maybe the "decorator" design pattern will be more useful here? sourcemaking.com/design_patterns/decoratorMousy
Why are you using a builder in C++? :( You have a constructor and can use defaults...Dichroite
@erip, I gave a pretty long description of why I'm using a builder in C++. Maybe I didn't mention however that there are no defaults for a node. But I did say there are mutliple ways of creating (building) a node.Flyleaf
@AlexLop. wouldn't the decorator also have the same problem, in that a not fully decorated object can be created? and it would have the downside of creating a lot of objects.Flyleaf
You could go with expression templates. If each withX function returns a different type evaluation of build() could check (at compile time) if conditions are fulfilled. But all that's quite a challenge.Ritualize
@ClaasBontus That exactly what I thought (returning distict types), but with templates, which is even better ;)Eboh
@erip, I don't see how this is relevant or better. Some defaults aren't simple, eg a std::function. defaults are for optional parameters, none of them here are optional. the assertion will be at runtime. You can't specify a combination that doesn't start at the beginning without reproviding the default values.Flyleaf
@ClaasBontus, while that article is extremely dense and will take some time to read. I thought about using template to return different types as Drop says, but won't that mean a lot of temporary objects are created?Flyleaf
You might also want to consider Boost DI.Ritualize
W
9

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

Whoso answered 20/5, 2016 at 17:25 Comment(2)
I've done something very similar in the past and it did work pretty well; IMHO it's much better than a runtime check (which would need to store the fields in every instance of every Node).Myrwyn
you could even remove the build() function and just return the object once the bitmask is completeRomulus
F
2

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
Flyleaf answered 23/5, 2016 at 13:1 Comment(0)
E
0

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.

Eboh answered 20/5, 2016 at 10:49 Comment(0)
S
0

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.

Stonebroke answered 20/5, 2016 at 11:53 Comment(0)
S
0

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.

Serve answered 23/8, 2018 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.