构建器模式:确保对象完全构建

时间:2016-05-20 10:16:47

标签: c++ c++11 builder builder-pattern

例如,如果我有一个构建器,那么我可以创建这样的对象:

Node node = NodeBuilder()
            .withName(someName)
            .withDescription(someDesc)
            .withData(someData)
            .build();

如何确保在构建方法之前已经设置了用于构建对象的所有变量?

例如:

Node node = NodeBuilder()
            .withName(someName)
            .build();

不是一个有用的节点,因为尚未设置描述和数据。

我使用构建器模式的原因是因为没有它,我需要很多构造函数的组合。例如,可以通过获取Field对象来设置名称和描述,并且可以使用文件名设置数据:

Node node = NodeBuilder()
            .withField(someField) //Sets name and description 
            .withData(someData) //or withFile(filename)
            .build(); //can be built as all variables are set

否则将需要4个构造函数(字段,数据),(字段,文件名),(名称,描述,数据),(名称,描述,文件名)。当需要更多参数时会变得更糟。

这些“方便”方法的原因是因为必须构建多个节点,因此它可以节省大量重复的行,如:

Node(modelField.name, modelField.description, Data(modelFile)),
Node(dateField.name, dateField.description, Data(dateFile)),
//etc

但是在某些情况下,需要使用非来自文件的数据构建节点,和/或名称和描述不基于字段。此外,可能有多个节点共享相同的值,因此不是:

Node(modelField, modelFilename, AlignLeft),
Node(dateField, someData, AlignLeft),
//Node(..., AlignLeft) etc

你可以:

LeftNode = NodeBuilder().with(AlignLeft);

LeftNode.withField(modelField).withFile(modelFilename).build(),
LeftNode.withField(dateField).withData(someData).build()

所以我认为我的需求与构建器模式非常匹配,除了构建不完整对象的能力。由于上述原因,“在构造函数中放置必需参数并具有可选参数的构建器方法”的正常建议不适用于此处。

实际问题:如何在编译时调用构建之前确保已设置所有参数?我正在使用C ++ 11。

(在运行时我可以为每个参数设置一个标志位,并声明所有标志都在构建中设置)

或者还有一些其他模式可以处理大量的构造函数组合吗?

5 个答案:

答案 0 :(得分:8)

免责声明:这只是一个快速拍摄,但我希望它可以让您了解您的需求。

如果您希望这是编译器时间错误,编译器需要知道构造的每个阶段当前设置的参数。您可以通过为当前设置的参数的每个组合设置不同的类型来实现此目的。

template <unsigned CurrentSet>
class NodeBuilderTemplate

这使得设置参数成为NodeBuilder类型的一部分; CurrentSet用作位字段。现在,您需要为每个可用参数添加一些内容:

enum
{
    Description = (1 << 0),
    Name = (1 << 1),
    Value = (1 << 2)
};

您从没有设置参数的NodeBuilder开始:

typedef NodeBuilderTemplate<0> NodeBuilder;

每个setter必须返回一个新的NodeBuilder,并将相应的位添加到位域:

NodeBuilderTemplate<CurrentSet | BuildBits::Description> withDescription(std::string description)
{
    NodeBuilderTemplate nextBuilder = *this;
    nextBuilder.m_description = std::move(description);
    return nextBuilder;
}

现在,您可以在static_assert函数中使用build来确保CurrentSet显示设置参数的有效组合:

Node build()
{
    static_assert(
        ((CurrentSet & (BuildBits::Description | BuildBits::Name)) == (BuildBits::Description | BuildBits::Name)) ||
        (CurrentSet & BuildBits::Value),
        "build is not allowed yet"
    );

    // build a node
}

每当有人试图在build()上调用缺少某些参数的NodeBuilder时,就会触发编译时错误。

正在运行示例:http://coliru.stacked-crooked.com/a/8ea8eeb7c359afc5

答案 1 :(得分:1)

我最终使用模板返回不同类型,并且只在最终类型上使用构建方法。但是,每次设置参数时它都会复制:

(使用来自Horstling的代码,但修改为我的方式)

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;
};

例如:

NodeBuilder BaseNodeBuilder = NodeBuilder().withDescription(" hello world");

Node n1 = BaseNodeBuilder().withName("Foo"); //won't compile
Node n2 = BaseNodeBuilder().withValue("Bar").withName("Bob"); //will compile

答案 2 :(得分:0)

免责声明:这是一个想法。我不确定它是否有效。只是分享。

您可以尝试:

  • build()
  • 中移除NodeBuilder方法
  • 将您的必填字段重新组合为NodeBuilder的单个构建方法,例如NodeBuilder::withFieldData(bla, bli, blu)和/或NodeBuilder::withFieldData(structBliBlaBLU)
  • withFieldData()返回不同类型的构建器,例如NodeBuilderFinal。只有这种类型的构建器具有build()方法。您可以从NodeBuilder继承非强制性方法。 (严格来说,NodeBuilderFinal是“代理”对象)

这将强制用户在withFieldData()之前调用build(),同时允许以任意顺序调用其他构建器方法。在非最终构建器上调用build()的任何尝试都将触发编译器错误。在完成最终构建之前,build()方法不会显示在自动完成中。)。

如果您不想要单一withFieldData方法,则可以从每个“字段”方法返回不同的代理,例如NodeBuilderWithNameNodeBuilderWithFile,并且可以从这些方法返回{ {1}}等等,直到构建最终构建器。这非常多毛,需要引入许多课程来涵盖不同的“现场”呼叫顺序。与@ClaasBontus在评论中提出的内容类似,您可以使用模板对其进行概括和简化。

理论上,您可以尝试通过在链中引入更多代理对象来强制执行更复杂的约束。

答案 3 :(得分:0)

我能想象的唯一方法是为每个返回构建器实例的必需参数设置一个静态构建器方法(或构造函数),然后设置(或覆盖)参数的简单实例方法返回实例。

它将允许编译时间检查,但是代价要复杂得多,所以我强烈建议你不要使用它,除非你有充分的理由去做。

答案 4 :(得分:0)

这个问题不能过时。让我分享我对这个问题的解决方案。

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

};

下面定义了class Car

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

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

build中的.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;
}

也许有点复杂,但在客户端看起来不错:

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

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

如果您错过build()之前的内容,则会在编译时发生错误。另一个缺点是论据的严格顺序。 可以与可选参数结合使用。