Builder 模式关注于创建复杂对象 (creation of complicated object ).
2.1 Scenario 考虑下面一个简单的例子. 我们要生成一段HTML的代码:
1 2 3 4 5 6 7 8 9 10 string words[] = {"hello", "world"}; ostringstream oss; oss << "<ul>"; for( auto w : words) { oss << " <li>" << w << "</li>"; } oss << "</ul>"; printf(oss.str().c_str());
首先想到的是定义一个HtmlElement
来存储每个tag的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct HtmlElement { string name; string text; vector<HtmlElement> elements; HtmlElement(){} HtmlElement{const string& name, const string& text} : name(name), text(text) {} string str(int indent = 0) const { // pretty-print the contents. } }
这样我们可以这样使用:
1 2 3 4 5 6 7 8 string words[] = {"hello", "world"}; HtmlElement list{"ul", ""}; for( auto w: words) { list.elements.emplace_back{HtmlElement{"li", w}}; } printf(list.str().c_str());
这样构建每个HtmlElement
仍然不是很方便, 我们可以继续改进Builder模式:
2.2 Simiple Builder Builder模式 的目的是把构建一个类的细节放到单独的一个类中进行. 我们的第一步尝试是使用一个HtmlBuilder
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct HtmlBuilder { HtmlElement root; HtmlBuilder(string root_name){ root.name=root_name; } void add_child(string chile_name, string child_text) { HtmlElement e{child_name, child_text}; root.elements.emplace_back(e); } string str() { return root.str(); } }
上面这个类提供了add_child()
方法用来添加当前项的子项:
1 2 3 4 HtmlBuilder builder{"ul"}; builder.add_child("li", "hello"); builder.add_child("li", "world"); cout << builder.str() << endl;
2.3 Fluent Builder 我们可以把add_child()
改写成下面的样子:
1 2 3 4 5 6 HtmlBuilder& add_child(string child_name, string child_text) { HtmlElement e{child_name, child_text}; root.elements.emplace_back(e); return *this; }
通过返回当前对象的引用, 我们可以把builder的方法连起来使用:
1 2 3 4 5 HtmlBuilder builder{"ul"}; builder.add_child("li", "hello") .add_child("li", "world"); cout << builder.str() << endl;
当前, 是返回引用还是返回指针完全由你自己来决定:
1 2 3 4 5 6 HtmlBuilder* add_child(string child_name, string child_text) { HtmlElement e{child_name, child_text}; root.elements.emplace_back(e); return this; }
相应的, 使用也变为:
1 2 3 4 5 HtmlBuilder builder{"ul"}; builder->add_child("li", "hello") ->add_child("li", "world"); cout << builder.str() << endl;
2.4 Communicating Intent 定义了Builder方法之后, 如何让用户使用它而不是仍然使用构造函数呢? 最简单的做法是强制用户使用. 我们通过将构造函数保护起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct HtmlElement { string name; string text; vector<HtmlElement> elements; const size_t indent_size = 2; static unique_ptr<HtmlBuilder> build(const string& root_name) { return make_unique<HtmlBuilder>(root_name); } protected: HtmlElement(){} HtmlElement(const string& name, const string& text) : name(name), text(text) {} };
上面的代码有两点:
我们将HtmlElement
的构造函数设置为protected , 从而组织了客户端访问
我们创建了一个静态**工厂方法(Factory Method)**来创建一个Builder.
我们这样使用它:
1 2 3 4 5 auto builder = HtmlElement::build("ul"); builder.add_child("li", "hello") .add_child("li", "world"); cout << builder.str() << endl;
但是, 我们的最终目的是创建一个HtmlElement
对象, 而不是一个builder
. 我们还可以继续改进HtmlBuilder
:
1 2 3 4 5 6 struct HtmlBuilder { operator HtmlElement() const { return root; } HtmlElement root; // 其他代码... }
上面代码的另一种变体是return std::move(root)
. 我们可以将其留给编译器来做.
这样, 我们的客户端代码就变为:
1 2 3 4 HtmlElement e = HtmlElement::build("ul") .add_child("li", "hello") .add_child("li", "world"); cout << e.str() << endl;
2.5 Groovy-Style Builder 诸如Groovy, Koltin等语言往往吹嘘说他们支持构建内奸的DSL语言, 能够以更方便的方式来构造类. C++一样能做.
1 2 3 4 5 6 7 8 9 10 11 12 struct Tag { std::string name; std::string text; std::vector<Tag> children; std::vector<std::pair<std::string, std::string>> attributes; friend std::ostream& operator<< (std::ostream& os, const Tag& tag) { // ... } };
现在, 我们有了一个Tag
类, 它保存name
, text
, children
和HTML attributes
. 我们还需要提供一些pretty printing代码.
接下来提供一些protected
的构造函数( 避免被客户端直接调用):
1 2 3 4 5 6 7 8 9 10 11 12 struct Tag { ... protected: Tag(const std::string& name, const std::string& text) : name(name), text(text) {} Tag(const std::string& name, const std::vector<Tag>& children) : name(name), children{children} {} };
现在, 我们可以从Tag
类派生出HTML的成分子类. 例如, 下面派生出用来表示段落的P
和表示图片的IMG
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct P : Tag { explicit P(const std::string& text) : Tag("p", text) {} P(std::initializer_list<Tag> childern) : Tag("p", children) {} }; struct IMG : Tag { explicit IMG(const std::string& url) : Tag("img", "") { attributes.emplace_back({"src", url}); } };
现在, 我们的代码可以这样写了:
{.line-numbers} 1 2 3 4 5 6 7 8 std::cout << P { IMG {"http://pokemon.com/pikachu.png" } } << std::endl;
2.6 Composite Builder 接下来讨论使用两个builder来构建一个对象的情况.
例如, 我们考虑一个记录了个人信息的Person
类:
1 2 3 4 5 6 7 8 9 10 11 class Person { std::string street_address, post_code, city; std::string company_name, position; int annual_incom = 0 ; Person (){} };
下面是它的类图:
首先我们实现PersonBuilderBase
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class PersonBuilderBase { protected: Person& person; explicit PersonBuilderBase(Person& person) : person(person) {} public: operator Person() { return std::move(person); } // builder facets PersonAddressBuilder lives() const; PersonJobBuilder works() const; };
注意点:
这里的Person
使用的是引用. 这很重要 : 基类中并不实际保存person的实例, 而是仅仅保存了一个引用.
(其他的都没有什么好说的.)
接下来是定义PersonBuilder:
1 2 3 4 5 6 class PersonBuilder : public PersonBuilderBase { Person p; // 要构建的对象, 在这里保存 public: PersonBuilder() : PersonBuilderBase(p) {} }
然后是两个子Builder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class PersonAddressBuilder : public PersonBuilderBase { typedef PersonAddressBuilder self; public: explicit PersonAddressBuilder(Person& person) : PersonBuilderBase{person} {} self& at(const std::string& street_address) { person.street_address = stress_address; return *this; } self& with_postcode(const std::string& post_code){...} self& in(const std::string& city) {...} }; class PersonJobBuilder : public PersonBuilderBase { ... }
这样, 客户端代码可以这样写:
{.line-numbers} 1 2 3 4 5 6 7 Person p = Person::create () .lives ().at ("123 London Road" ) .with_postcode ("SW1 1GB" ) .in ("London" ) .works ().at ("PragmaSoft" ) .as_a ("Consultant" ) .earing (10e6 );
2.7 Summary …