设计模式
第 2 章 构造器模式
构造器模式(Builder)主要关注复杂对象的创建过程。复杂对象指的是难以通过调用简单的构造函数来创建的对象。这些类型的对象本身可能由其他对象组成,并且可能设计不明显的逻辑,因此需要专门设计单独的组件来创建。
2.1 预想方案
当前有一个HtmlElement类, 用于存储每一类标签的信息:
struct HtmlElement
{
std::string name;
std::string text;
std::vector<HtmlElement> elements;
HtmlElement() {
}
HtmlElement(std::string name, std::string text)
:name(name),text(text) {
}
// 该函数用来实现把elements元素拼接为字符串
std::string str(int ident = 0) {
// 考虑到元素的子元素包含子元素的情况,该方法可以采用递归处理起来会方便点
// 递归法
if (elements.empty()) {
return "<" + name + "> " + text + " </" + name + ">";
}
std::string res;
res += "<" + name + ">" + text;
for(auto e: elements) {
res += e.str(); // 递归处理, 对子元素的的子元素处理
}
res += " </" + name + ">";
return res;
}
};
现在我们用这个类来创建列表。创建的方法如下:
void oop_create_html()
{
std::string words[] = {"hello", "world"};
HtmlElement list{"ul", ""};
for (auto w: words) {
list.elements.emplace_back("li", w);
}
std::cout << list.str() << std::endl;
}
这种方法虽然不错, 但是每个 HtmlElement 的构建过程不是很方便,我们可以通过构造器模式改进它。
2.2 简单构造器
构造器模式尝试将对象的分段构造过程封装到单独的类中。以下代码初步尝试使用构造器:
struct HtmlBuilder
{
HtmlElement root;
HtmlBuilder(std::string root_name) {
root.name = root_name;
}
// 向当前的元素中添加子元素
void add_child(std::string child_name, std::string child_text) {
root.elements.emplace_back(child_name, child_text);
}
std::string str() {
return root.str();
}
};
上述代码中的HtmlBuilder是专门同于构建HTML元素的组件。add_child方法用于向当前的元素中添加其它子元素,每个子元素是“名字 - 文本”对。HtmlBuilder的使用方法如下:
void simple_constructor()
{
HtmlBuilder builder("ul");
builder.add_child("li", "hello world.");
builder.add_child("li", "what that?");
std::cout << builder.str() << std::endl;
}
可以看到,此时add_child()方法返回的是void。方法的返回值可以有很多用处,其中最常用的是帮助我们创建流式接口。
2.3流式构造器
现在,我们将add_child()方法的定义修改如下:
HtmlBuilder& add_child(std::string child_name, std::string child_text) {
root.elements.emplace_back(child_name, child_text);
return *this;
}
通过将add_child()方法的返回值修改位对HtmlBuilder的引用,现在Builder可以实现链式调用。这就是所谓的流式接口。
void flow_constructor()
{
HtmlBuilder builder{"ul"};
builder.add_child("li", "hello world")
.add_child("li", "flow constructor");
std::cout << builder.str() << std::endl;
}
使用引用还是指针完全取决于格式。如果我们想要通过 -> 运算符实现链式调用,则可以将add_child()定义如下:
HtmlBuilder* add_child2(std::string child_name, std::string child_text) {
root.elements.emplace_back(child_name, child_text);
return this;
}
然后通过->调用:
void flow_constructor2()
{
HtmlBuilder *builder = new HtmlBuilder("ul");
builder->add_child2("li", "hello world")
->add_child2("li", "flow constructor 2");
std::cout << builder->str() << std::endl;
}
2.4 向用户传达意图
现在我们已经有一个用于创建HTML元素的专用的HtmlBuilder,但是怎样使用户知道HtmlBuilder的使用方法呢?一种办法是,强制用户在构建对象时使用HtmlBuilder。以下是一种可行的做法:
struct HtmlElement2
{
friend struct HtmlBuilder2;
std::string name;
std::string text;
std::vector<HtmlElement2> elements;
const size_t indent_size = 2;
private:
HtmlElement2() {
}
HtmlElement2(std::string name, std::string text)
:name(name),text(text) {
}
public:
static std::unique_ptr<HtmlBuilder2> create(const std::string &root_name) {
return std::make_unique<HtmlBuilder2>(root_name);
}
// 该函数用来实现把elements元素拼接为字符串
std::string str(int ident = 0) {
// 考虑到元素的子元素包含子元素的情况,该方法可以采用递归处理起来会方便点
// 递归法
if (elements.empty()) {
return "<" + name + "> " + text + " </" + name + ">";
}
std::string res;
res += "<" + name + ">" + text;
for(auto e: elements) {
res += e.str(); // 递归处理, 对子元素的的子元素处理
}
res += " </" + name + ">";
return res;
}
};
这一段代码首先隐藏了HtmlElement2的所有构造函数,所以无法在外部构造HtmlElement2。其次,创建了一个工厂方法,从HtmlElement2直接创建构造器。该方法是一种静态方法,使用如下:
void user_to_api()
{
auto builder = HtmlElement2::create("ul");
builder->add_child("li", "hwllo world")
.add_child("li", "user to api");
std::cout << builder->str() << std::endl;
}
首先,说明下这里为什么和《C++20设计模式》书中写的不一致。1、我认为书中的写法是错误的写法,create方法的返回结果是unique_ptr智能指针,所以我们应该先使用->访问,由于add_child()返回的结果是引用, 所以后续应该用“.” 运算符(当然,也可以像2.3节中说的,使用返回指针类型的方法,一直使用->运算符)。
其次,我们在来说下HtmlElement2 和 HtmlBuilder2 。以下是 HtmlBuilder2 的实现:
struct HtmlBuilder2
{
HtmlElement2 root;
HtmlBuilder2(std::string root_name) {
root.name = root_name;
}
HtmlBuilder2& add_child(std::string child_name, std::string child_text) {
// 由于HtmlElement2的构造函数为protected, 所以需要在HtmlElement2把HtmlBuilder2设位HtmlElement2的友元类
// 在这块遇到了个问题, 不能直接使用参数写到elements中,需把创建好的类型然后作为emplace_back的参数传入
// 该问题的原因是由于HtmlElement2的构造函数是保护成员,把child_name和child_text作为参数传入emplace_back
// vector内部会创建HtmlElement2导致报错
// root.elements.emplace_back(child_name, child_text);
root.elements.emplace_back(HtmlElement2(child_name, child_text));
return *this;
}
std::string str() {
return root.str();
}
};
对比2.3节及之前本章的内容。不难发现, 我这里的实现和之前的实现是有小小的差别(出本节前面提到的隐藏构造函数)。
1、需要把 HtmlBuilder2设为HtmlElement2 的友元类;
2、HtmlBuilder2中的add_child方法中的实现略有差异, 是把创建好的HtmlElement2 对象传入vector的emplace_back函数中(详细原因在代码HtmlBuilder2实现中有简单的写)。
前面我们所实现的都是基于add_child返回HtmlBuilder/ HtmlBuilder2的引用或者指针。但是,我们的目的应该是创建一个HtmlElement/ HtmlElement2,而不是它的构造器。所以我们可以调用运算符实现隐式转换,从而得到HtmlElement2(2.4节以HtmlElement2、HtmlBuilder2为例),在HtmlBuilder2中添加以下方法:
operator HtmlElement2() const {
return root;
}
增加运算符的代码可以这样写:
void user_to_api_add_op()
{
HtmlElement2 e = HtmlElement2::create("ul")
->add_child("li", "hwllo world")
.add_child("li", "user to api op");
std::cout << e.str() << std::endl;
}
遗憾的是, 没有一种明确的方法可以告知其他用户要以上面的方式来使用这个API。我们希望对构造函数的限制以及静态函数build()的存在能够让用户使用构造器。除了上述定义的运算符以外。还可以向HtmlBuilder2添加相应的build函数:
HtmlElement2 build() const{
return root;
}
2.5 Croovy风格的构造器(构建对象的一种备选方案)
Groovy、Kontlin和其他编成语言都试图通过支持易于处理的语法结构来展示他们在构建DSL方面有多么出色。借助C++初始化列表特性,我们可以使用普通的类来高效地构建与HTML兼容的DSL。
首先,定义一个HTML标签:
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)
{
os << "name: "<< tag.name << ", text:" << tag.text << std::endl;
return os;
}
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进行初始化)。
现在, 可以从Tag类派生出其他的类,但只能派生出一些有效的HTML标签类(从而限制了DSL)。接下来,我们定义两个标签,一个是段落标签,一个是图片标签:
struct P:Tag
{
// 在 C++ 中,explicit 是一个关键字,用于声明只能通过显式调用来调用的构造函数。
// 这可以防止编译器进行隐式类型转换,从而提高代码的可读性和安全性。
explicit P(const std::string& text)
:Tag("P", text) {
}
P(std::initializer_list<Tag> children)
:Tag("P", children) {
std::cout << "test." << std::endl;
}
};
struct IMG:Tag{
explicit IMG(const std::string& url)
:Tag("IMG", "") {
attributes.emplace_back("src", url);
}
};
上面的代码进一步限制了我们的API。根据这些构造函数,段落标签只能包含文本或一串子标签。另外,图片标签不能包含其他标签,只能有一个名为img的标签属性以及要加载的图片的地址。
现在, 我们借助前面定义的统一初始化过程和构造函数, 我们可以编写如下代码:
void test_groovy_style()
{
std::cout <<
P{
IMG{"http://baidu.com"}
}
<< std::endl;
P p{
IMG{"http://baidu.com"}
};
std::cout << p << std::endl;
}
我们已经位段落和图片构建了一种小型的DSL,整个过程完全看不到add_child的调用。而且这个模型可很容易的扩展来支持其他标签。
2.6 组合构造器
我们将使用多个构造器来构建单个对象的示例来结束对构造器模式的讨论。现在,假设我们想记录一个人的某些信息:
class Person{
// address
std::string street_address;
std::string post_code;
std::string city;
// employment
std::string company_name;
std::string position;
int annual_income = 0;
Person(){}
friend class PersonBuilder;
friend class PersonAddressBuilder;
friend class PersonJobBuilder;
public:
static PersonBuilder create();
};
Person有两方面的信息:地址信息和工作信息。如果我们用单独的构造器来分别创建不同的信息,我们该如何定义最实用的API呢?为此, 我们将创建一个组合构造器。这个创建过程很重要,所以请注意,尽管我们想正对工作信息和地址信息分别创建后早期,但我们将生成不少于四个不同的类。下图展示我们想要创建的类及其关系。
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;
};
这比之前的构造器复杂多了,接下来我们将逐个讨论PersonBuilderBase的各个成员:
- person: 这是对即将创建的对象的引用。看起来虽然奇怪,但这对创建Person对象的子构建器却很有意义!注意一个关键点,爱这个类中并没有实际存储Person的成员!这个类存储的是Person的引用,而不是实际构建的Person对象。
- 以Person的引用作为参数的构造函数被声明为protected,因此,只有派生类(PersonBuilder、PersonAddressBuilder和PersonJobBuilder)可以使用它。
- Operator Person()是之前的使用过的一个技巧,这里为Person提供一个移动构造函数
- lives()和works()是两个返回构造器的函数:他们分别完成对地址信息和工作信息的构建。
现在,基类中为一缺少的就是实际要构建的对象了,他存储在即将定义的派生类PersonBuilder中。这个类也是我们希望用户真正使用的类:
class PersonBuilder:public PersonBuilderBase {
protected:
Person p;
public:
PersonBuilder()
:PersonBuilderBase{p} {
}
};
PersonBuilder才是真正要构建Person类对象的地方。PersonBuilder不是用于继承的, 它只是一个构建构造器初始化过程的工具。
为什么我们要定义不同的public和protected构造函数?我们来看一下其中一个子构造器的实现:
class PersonAddressBuilder:public PersonBuilderBase {
typedef PersonAddressBuilder self;
public:
explicit PersonAddressBuilder(Person& person)
:PersonBuilderBase{person}{}
self& at(std::string street_address)
{
person.street_address = street_address;
return *this;
}
self& with_postcode(std::string post_code)
{
person.post_code = post_code;
return *this;
}
self& in(std::string city)
{
person.city = city;
return *this;
}
};
可以看到,PersonAddressBuilder提供了创建Person地址的流式接口。注意,PersonAddressBuilder实际上继承自PersonBuilderBase(这意味着它也有lives()和works()方法),并且将Person的引用传入PersonBuilder的构造函数。PersonAddressBuilder并没有继承PersonBuilder—如果它这样做了,我们将会创建很多Person的实例,但我们只需要一个就够了。
同样,PersonJobBuilder会以同样的方式来实现。PersonAddressBuilder、PersonJobBuilder和PersonBuilder,均声明为Person类的友元类,这样, 他们可以访问Person的私有成员。
现在,是见证如何构建Person的时候了!以下是示例代码:
void testCombinationConstructor()
{
Person p = Person::create()
.lives()
.at("123 London road")
.with_postcode("SW1 1GB")
.in("london")
.works()
.at("pragmaSoft")
.as_a("Consultant")
.earning(10e6);
}
我们使用create()函数得到了一个构造器,然后使用lives()方法获得了一个PersonAddressBuilder,一旦我们完成地址的初始化,然后通过调用works()方法就可以使用PersonJobBuilder了。
当完成构建过程后,我们可以像之前一样得到构建的Person的对象。注意,一旦构建完成,构造器就没有永乐,因为我们调用move()函数来移动Person。
总结:
在本节的学习中,遇到了一些C++编码问题, 开始的时候, 我把类成员函数的声明和定义是在一起的, 会报“invalid use of incomplete type ‘class PersonBuilder”问题和 “return type ‘class PersonAddressBuilder’ is incomplete”等其他同样的问题。其原因是因为类型不完整,经过网上查询(网上有的说是使用指针替换啥的,我用其方法及对应代码测试,并不能解决问题,这可能和我的编译器有关系,但是对于我来说,没解决我的问题),然后通过查询和实验,可以使用声明和定义分开的方式来解决这个问题。还有2.6节中的代码不是完整代码,我最终会放在指定的文件中。
2.7 参数化构造器
正如前面展示的示例,强制用户使用构造器而不是直接构造对象的唯一办法是使构造函数不可访问。然而,在某些情况下,我们可能想要明确地强制用户从一开始就与构造函数打交道,甚至我们可能希望隐藏他们正在构建的对象。
例如,假设我们有一个用于发送电子邮件夫人API,器内部 对电子邮件的描述如下:
class Email
{
public:
std::string from;
std::string to;
std::string subject;
std::string body;
// possibly other members here
};
内部是指不想让用户直接与这个类打交道,也许是因为Email类中存储了一些关于附加服务的信息。Email的部分内容是可选的(例如subject),所以Email对象不必设置所有的属性成员。
现在,我们决定实现一个流失构造器,通过这个构造器,用户将在幕后构建Email。以下代码是流式构造器的一种可能实现方式:
class EmailBuilder
{
Email &email;
public:
explicit EmailBuilder(Email &email) : email(email) {}
EmailBuilder &from(std::string from)
{
email.from = from;
return *this;
}
EmailBuilder &to(std::string to)
{
email.to = to;
return *this;
}
EmailBuilder &subject(std::string subject)
{
email.subject = subject;
return *this;
}
EmailBuilder &body(std::string body)
{
email.body = body;
return *this;
}
// other fluent members here
};
现在,为了强制用户只通过构造器发送邮件,我们可以实现一个MailService:
class MailService{
class Email{ ... };
public:
class EmailBuilder{...};
void send_email(std::function<void(EmailBuilder)> builder)
{
Email email;
EmailBuilder b{email};
builder(b);
send_email_impl(email);
}
private:
void send_email_impl(const Email& email)
{
// actually send the email
std::cout << "from: " << email.from << std::endl
<< "to: " << email.to << std::endl
<< "subject: " << email.subject << std::endl
<< "body: " << email.body << std::endl;
}
};
可以看到,用户使用的send_email()方法的入口并不是一组参数或预先打包好的对象,而是携带了一个函数。这个函数以EmailBuilder的引用为参数,然后通过EmailBuilder构建邮件的主体内容。一旦构建工作完成,我们就可以使用MailService的内部机制来生成一个完全初始化的Email.
可以看到,这里有一个巧妙的技巧:EmailBuilder通过其构造函数的参数来获得Email的引用,而不是在其内部存储Email的引用。这么作的原因是,通过这种方式,EmailBuilder也就不必在它的任何一个API中沟开暴露Email。
以下代码以用户的视角来展示这些API的使用方式:
void testParameterizedConstructor()
{
MailService ms;
ms.send_email([&](auto eb) {
eb.from("foo@bar.com")
.to("bar@baz.com")
.subject("hello")
.body("Hello, how are you?");
});
}
不论用户喜欢与否,参数化构造器模式强制用户通过我们提供的API来使用构造器。我们采用的这种基于函数的技巧能够确保用户获得已初始化的构造器对象。
2.8 构造器模式的继承性
一个有趣的问题时流式构造器的继承性,这个问题不仅影响着流式构造器,也影响着人一一个使用了流式接口的类。某个流式构造器可能继承自另一个流式构造器吗?答案是肯定的,但是并不容易。
这就是问题所在。假设,我们要构建一个很简单的对象:
class Person
{
public:
std::string name;
std::string position;
std::string date_of_birth;
friend std::ostream &operator<<(std::ostream &os, const Person &obj)
{
return os << "name: " << obj.name
<< "\nposition: " << obj.position
<< "\ndata_of_birth: " << obj.date_of_birth << "\n";
}
};
我们在定义一个基类PersonBuilder用于构建Person对象:
class PersonBuilder
{
protected:
Person person;
public:
// PersonBuilder(){}
// PersonBuilder(Person &person) : person{person} {}
// [[nodiscard]] :C++17引入的语法
// 当用于描述函数的返回值时,如果调用函数的地方没有获取返回值时,编译器会给予警告
// 当用于描述类或枚举时,若果函数的返回值是该类或枚举的对象时(引用和指针不可以),如果该返回值没有被获取,编译器给予警告。
[[nodiscard]] Person build() const
{
return person;
}
};
【注意】:[[nodiscard]]的作用有在代码中注释说明
接下来是一个专门用于构建Person名称的类:
class PersonInfoBuilder : public PersonBuilder
{
public:
// PersonInfoBuilder(){}
// explicit PersonInfoBuilder(Person &person) : PersonBuilder(person) {}
PersonInfoBuilder &called(const std::string &name)
{
person.name = name;
return *this;
}
};
这种做法可行,并且绝对没有任何问题。但现在,假设我们要从PersonInfoBuilder派生另一个类,以构建Person的工作信息。也许我们会编写如下代码:
class PersonJobBuilder : public PersonInfoBuilder
{
public:
// PersonJobBuilder(){}
// explicit PersonJobBuilder(Person &person) : PersonInfoBuilder(person) {}
PersonJobBuilder &works_as(const std::string &position)
{
person.position = position;
return *this;
}
};
不幸的是,现在我们已经破坏了流式接口,并且使得整个创建过程不再可用:
void testInheritanConstructor()
{
PersonInfoBuilder pb;
auto person = pb.called("Dmitri")
.work_as("programmer") // will not compile
.build();
}
【注】在测试的过程中发现有以下问题我们需要关注,因为上述测试代码我们会用到默认构造函数,我们定义的类是一条继承链上的类;所以我们要么不要定义带参构造函数,要么在定义的时候同时定义默认构造函数。否则,当我们定义非默认构造函数时,编译系统会默认删除默认构造函数。
上述代码将编译不通过,原因:called()方法返回的 *this 是PersonInfoBuilder& 类型的,但PersonInfoBuilder类并没有work_as()方法。
这个问题的解决办法是:我们可以使用继承性设计流式API,重新设计PersonInfoBuilder如下:
template<typename TSelf>
class PersonInfoBuilder : public PersonBuilder
{
public:
// PersonInfoBuilder(){}
// explicit PersonInfoBuilder(Person &person) : PersonBuilder(person) {}
TSelf &called(const std::string &name)
{
person.name = name;
return static_cast<TSelf&>(*this);
// alternatively, *static_cast<TSelf*>(this)
}
};
这是经典的CRTP,我们引入了一个新的模板参数TSelf。我们期待这个参数继承自PersonInfoBuilder<TSelf>。这也许看起来很奇怪,尤其是没有concept或static_assert—遗憾的是,C++无法完成像这样的自我检查,因为想要完成某个类的自我检查时,尚未对这个类进行完整的定义。
流式接口继承最大的问题在于,在调用基类的流式接口时,能否将其基类内部的this指针转换为正确的类型并作为该流式接口的返回值。解决这个问题为一有效的方法是使用一个贯穿整个继承层次的模板参数(TSelf)。
为了理解这一点,我们定义PersonJobBuilder如下:
template <typename TSelf>
class PersonJobBuilder : public PersonInfoBuilder<PersonJobBuilder<TSelf>>
// class PersonJobBuilder : public PersonInfoBuilder<TSelf>
{
public:
// explicit PersonJobBuilder(Person &person) : PersonInfoBuilder(person) {}
TSelf &work_as(const std::string &position)
{
this->person.position = position;
return static_cast<TSelf &>(*this);
}
};
注意看PersonJobBuilder的基类, 不再是之前的那个普通的PersonInfoBuilder,而是PersonInfoBuilder<PersonJobBuilder<TSelf>>。因此当继承自PersonInfoBuilder时,我们将PersoInfoBuilder的TSelf参数设置为PersonJobBuilder,以使基类的所有流式接口返回正确的类型,而不是基类本身的类型。
现在在加入另一个属性date_of_birth和对应的PersonDateOfBirthBuilder。那么应该继承自那个类呢?
如果认为是继承PersonInfoBuilder<PersonJobBuilder<PersonDateOfBirthBuilder<TSelf>>>,那就错了。因为PersonJobBuilder已经是一个PersonInfoBuilder了,所以PersonInfoBuilder不必在出现在继承关系链中。相反,我们将PersonDateOfBirthBuilder定义如下:
template <typename TSelf>
// class PersonBirthOfDataBuilder : public PersonJobBuilder<TSelf>
class PersonBirthOfDataBuilder : public PersonJobBuilder<PersonBirthOfDataBuilder<TSelf>>
{
public:
TSelf &born_on(const std::string &data_of_birth)
{
this->person.date_of_birth = data_of_birth;
return static_cast<TSelf &>(*this);
}
};
最后一个问题是,鉴于这些类都带有模板参数,我们将如何构造这样的构造器呢?我们需要一个新的类型,而不仅是一个变量。所以,我们需要i在某个地方编写如下的代码:
class MyBuilder : public PersonBirthDataBuilder<MyBuilder>
{
};
这可能是最令人厌烦的一个实现细节;为了使用上面定义的构造器,我们需要从递归的带有模板参数的类派生出不带模板参数的类。
也就是说,现在我们可以利用MyBuilder继承链中的方法将所有内容放在一起:
void testInheritanConstructor()
{
MyBuilder mb;
auto me = mb.called("Dmitri")
.work_as("programmer") // will not compile
.born_on("01/01/1980")
.build();
std::cout << me;
// printf("test: %s",typeid(me).name());
}
【问题】分析及测试,无论called、work_as还是born_on返回的最终类型都是MyBuilder类型,我们把继承关系中继承的模板都统一为<TSelf>不同样也可以吗?即改为在PersonJobBuilder和PersonBirthOfDataBuilder中注释的代码中模板的继承方法,不一样可以实现这个要求吗?
2.9 总结
构造器模式的目的是简化复杂对象或一系列对象的构建过程,从而单独定义构成该复杂对象的各个组件的构建方法。通过前面的内容学习,我们已经观察到构造器模式有以下的特点:
- 构造器模式可以通过流式接口调用链来实现复杂的构建过程。为了实现流式接口,构造器函数需要返回this或 *this。
- 为了强制用户使用构造器的API,我们可以将目标对象的构造函数限制为不可访问,同时,定义一个create()接口返回构造器。
- 通过定义适当的运算符,可以使构造器转化为对象本身。
- 借助C++新特性中的统一初始化语法,可以实现Groovy风格的构造器。这是一个通用的方法,可以创建各式各样的DSL。
- 单个构造器接口可以暴露多个子构造器接口,通过灵活地使用继承和流式接口,很容易将一个构造器变换为另一个构造器。
再次说明,当对象的构建过程是非普通的时候,构造器模式是有意义的。对于那些通过数量有限且命名合理的构造函数参数来明确构造的简单对象而言,它们应该使用构造函数(或依赖注入),而不必使用构造器模式。