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

上面的代码有两点:

  1. 我们将HtmlElement的构造函数设置为protected, 从而组织了客户端访问
  2. 我们创建了一个静态**工厂方法(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, childrenHTML 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
{
// address
std::string street_address, post_code, city;
// employment
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;
};

注意点:

  1. 这里的Person使用的是引用. 这很重要: 基类中并不实际保存person的实例, 而是仅仅保存了一个引用.

  2. (其他的都没有什么好说的.)

接下来是定义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