2016-05-20 7 views
7

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

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.

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

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

+1

Флаг путь, вероятно, самый простой способ, а затем функция 'build' выдает исключение, если обязательное поле не задано. Я не могу придумать никакого способа сделать это проверкой времени компиляции. –

+0

Возможно, дизайн шаблона «декоратор» будет более полезен здесь? https://sourcemaking.com/design_patterns/decorator –

+0

Почему вы используете построитель на C++? :(У вас есть конструктор и вы можете использовать значения по умолчанию ... – erip

ответ

0

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

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

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

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

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

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

0

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

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

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

+0

Я сделал что-то очень похожее в прошлом, и все получилось очень хорошо; ИМХО это намного лучше, чем проверка времени выполнения (которая должна была бы хранить поля в каждом экземпляре каждого Узла). –

+0

вы можете даже удалить функцию build() и просто вернуть объект после завершения битмаски – Arvid

0

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

(с использованием кода из 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