Шаблон построителя: убедитесь, что объект полностью построен

Если, например, у меня настроен конструктор, поэтому я могу создавать такие объекты:

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

Как я могу убедиться, что все переменные, используемые для построения объекта, были установлены до метода сборки?

Eg:

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()

Поэтому я думаю, что мои потребности вполне соответствуют шаблону конструктора, за исключением возможности создавать незавершенные объекты. Обычная рекомендация «поместить обязательные параметры в конструктор и использовать методы компоновщика для необязательных параметров» здесь не применяется по вышеуказанным причинам.

Актуальный вопрос: как я могу убедиться, что все параметры были установлены до вызова сборки во время компиляции? Я использую С++11.

(Во время выполнения я могу просто установить биты флага для каждого параметра и утверждать, что все флаги установлены в сборке)

В качестве альтернативы существует ли какой-либо другой шаблон для работы с большим количеством комбинаций конструкторов?


person Jonathan.    schedule 20.05.2016    source источник
comment
Путь с флагом, вероятно, самый простой, а затем функция build выдает исключение, если обязательное поле не установлено. Я не могу придумать, как сделать это проверкой во время компиляции.   -  person Some programmer dude    schedule 20.05.2016
comment
Может быть, шаблон проектирования декоратора будет более полезен здесь? sourcemaking.com/design_patterns/decorator   -  person Alex Lop.    schedule 20.05.2016
comment
Почему вы используете конструктор на C++? :( У вас есть конструктор и вы можете использовать значения по умолчанию...   -  person erip    schedule 20.05.2016
comment
@erip, я довольно подробно описал, почему я использую конструктор на C++. Однако, возможно, я не упомянул, что для узла нет значений по умолчанию. Но я сказал, что есть несколько способов создания (построения) узла.   -  person Jonathan.    schedule 20.05.2016
comment
@АлексЛоп. разве у декоратора не будет той же проблемы, поскольку может быть создан не полностью декорированный объект? и у этого была бы обратная сторона создания большого количества объектов.   -  person Jonathan.    schedule 20.05.2016
comment
Вы можете использовать шаблоны выражений. Если каждая функция withX возвращает другой тип, оценка build() может проверять (во время компиляции), выполняются ли условия. Но все это довольно сложная задача.   -  person Claas Bontus    schedule 20.05.2016
comment
@ClaasBontus Это именно то, о чем я думал (возвращение типов distict), но с шаблонами, что еще лучше;)   -  person Ivan Aksamentov - Drop    schedule 20.05.2016
comment
@erip, я не понимаю, насколько это актуально или лучше. Некоторые значения по умолчанию непросты, например, std::function. значения по умолчанию для необязательных параметров, ни один из них здесь не является необязательным. утверждение будет во время выполнения. Вы не можете указать комбинацию, которая не начинается с самого начала, без повторного предоставления значений по умолчанию.   -  person Jonathan.    schedule 20.05.2016
comment
@ClaasBontus, а эта статья очень насыщенная, и ее чтение займет некоторое время. Я думал об использовании шаблона для возврата разных типов, как говорит Дроп, но не будет ли это означать, что будет создано много временных объектов?   -  person Jonathan.    schedule 20.05.2016
comment
Вы также можете рассмотреть возможность Boost DI.   -  person Claas Bontus    schedule 24.05.2016


Ответы (5)


Отказ от ответственности: это всего лишь быстрый снимок, но я надеюсь, что он даст вам представление о том, что вам нужно.

Если вы хотите, чтобы это была ошибка времени компилятора, компилятор должен знать о текущих установленных параметрах на каждом этапе построения. Этого можно добиться, имея отдельный тип для каждой комбинации установленных в данный момент параметров.

template <unsigned CurrentSet>
class NodeBuilderTemplate

Это делает установленные параметры частью типа NodeBuilder; CurrentSet используется как битовое поле. Теперь вам нужно немного для каждого доступного параметра:

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

Вы начинаете с NodeBuilder, для которого не заданы параметры:

typedef NodeBuilderTemplate<0> NodeBuilder;

И каждый установщик должен возвращать новый 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.

person Horstling    schedule 20.05.2016
comment
Я делал что-то очень похожее в прошлом, и это работало очень хорошо; ИМХО, это намного лучше, чем проверка во время выполнения (которая должна хранить поля в каждом экземпляре каждого узла). - person Stefan Atev; 20.05.2016
comment
вы даже можете удалить функцию build() и просто вернуть объект после завершения битовой маски - person Arvid; 21.05.2016

В итоге я использовал шаблоны для возврата разных типов и имел метод сборки только для окончательного типа. Однако он делает копии каждый раз, когда вы устанавливаете параметр:

(используя код от 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
person Jonathan.    schedule 23.05.2016

Отказ от ответственности: это идея. Я даже не уверен, что это работает. Просто поделился.

Вы можете попробовать:

  • удалить build() метод из NodeBuilder
  • перегруппируйте свои обязательные поля в единый метод построения NodeBuilder, скажем, NodeBuilder::withFieldData(bla, bli, blu) и/или NodeBuilder::withFieldData(structBliBlaBLU).
  • make withFieldData(), чтобы вернуть строитель другого типа, скажем, NodeBuilderFinal. Только этот тип строителя имеет метод build(). Вы можете наследовать необязательные методы от NodeBuilder. (Строго говоря, NodeBuilderFinal — это «прокси-объект»)

Это заставит пользователя вызывать withFieldData() перед build(), позволяя вызывать другие методы компоновщика в произвольном порядке. Любая попытка вызвать build() для нефинального компоновщика вызовет ошибку компилятора. build() метод не будет отображаться в автодополнении, пока не будет сделан окончательный билдер ;).

Если вам не нужен монолитный метод withFieldData, вы можете вернуть разные прокси из каждого "полевого" метода, например NodeBuilderWithName, NodeBuilderWithFile, и из них вы можете вернуть NodeBuilderWithNameAndFile и т. д., пока не будет построен окончательный билдер. Это довольно сложно и потребует введения множества классов для покрытия различных порядков "полевых" вызовов. Подобно тому, что @ClaasBontus предложил в комментариях, вы, вероятно, можете обобщить и упростить это с помощью шаблонов.

Теоретически вы можете попытаться применить более сложные ограничения, введя в цепочку больше прокси-объектов.

person Ivan Aksamentov - Drop    schedule 20.05.2016

Единственный способ, который я могу себе представить, - это иметь несколько статических методов построителя (или конструкторов), по одному для каждого набора требуемых параметров, которые возвращали бы экземпляр построителя, а затем простые методы экземпляра для установки (или перезаписи) параметров и которые возвращают пример.

Это позволит проверять время компиляции, но за счет гораздо более сложного API, поэтому я настоятельно рекомендую вам не использовать его, если у вас действительно нет веских причин для этого.

person Serge Ballesta    schedule 20.05.2016

Этот вопрос не может быть устаревшим. Поделюсь своим решением этой проблемы.

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(). Еще одним недостатком является строгий порядок аргументов. Его можно комбинировать с необязательными параметрами.

person Sklert    schedule 23.08.2018