家里快没菜了,怎么办

4.1 构造对象

大部分的对象是使用构造函数来创建的. 但是, 如果你已经有了一个已经配置好了的对象, 为什么不利用它来简单的拷贝和修改来创建新的对象而非要一步步从头创建呢? 尤其是在你必须使用Builder模式来分段构造对象的时候.

考虑下面这段存在重复性的代码:

{.line-numbers}
1
2
Contact john{ "John Doe", Address{"123 East Dr", "London", 10 } };
Contact jane{ "Jane Doe", Address{"123 East Dr", "London", 11 } };

原型模式Prototype pattern就是关于如何进行对象拷贝的. 没有什么统一的方法, 但是有一些建议.

4.2 Ordinary Duplicate

如果你要拷贝的是值(value), 并且你要拷贝的对象存储了自己的所有的值, 那么没有什么问题. 例如:

1
2
3
4
5
6
7
8
9
10
11
struct Address
{
string street, city;
int suite;
}

struct Contact
{
string name;
Address address;
}

使用:

1
2
3
4
5
Contact worker{"", Address{"123 east Dr.", "London", 0}};

Contact john = worker;
john.name = "John Doe";
john.address.suit = 10;

在实践中, 上述情况很少发生. 在更多的情况下, 内部的Address对象会是一个指针:

1
2
3
4
5
struct Contact
{
string name;
Address* address; // 指针, 例如, shared_ptr
}

在这种情况下, 如果再用前面的客户端代码, 就会出问题: 此时, John和原型的address指向的是同一个地址.

4.3 Duplication via Copy Construction

最简单的做法是拷贝构造函数中生成Address的实例. 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Contact
{
...
Contact(const Contact& other)
: name(other.name)
//, address{new Address{*other.address}}
{
address = new Address(
other.address->street,
other.address->city,
other.address->suite
);
}
}

这个做法可用, 但是并不通用. 例如, 如果Address是更复杂的类, 也需要类似的处理, 该怎么办? 一个合理的做法是给Address也定义拷贝构造函数:

1
2
3
4
5
6
7
8
class Address
{
...
Address(const string& street, const string& city, const int suit)
: street{street}, city{city}, suite{suite}
{
}
}

这样, Contact的拷贝构造函数就可以写成:

1
2
3
Contact(const Contact& other)
: name(other.name)
, address(new Address{*other.address})

如果能够提供=操作符

1
2
3
4
5
6
7
8
9
10
Contact& operator = (const Contact& other)
{
if( this==&other)
return *this;

name = other.name;
// 注意, 我们同样为Address重载了赋值操作符
address = other.address;
return *this;
}

这样, 我们就可以这样写:

1
2
3
4
5
Contact worker{"", new Address{"123 East Dr", "London", 0}};
Contact john{worker};
// 或者 Contact john = worker
john.name = "john";
john.suite = 10;

另一种做法是提供一个明确的说明:

1
2
3
4
5
template <typename T>
struct Cloneable
{
virtual T clone() = 0;
}

不管使用哪种方法都是有效的, 但是会有一些啰嗦, 如果对象是很复杂的话.

4.4 Serialization的问题

不幸的是, C++没有提供任何序列号的帮助手段. 因为其他的语言编译的二进制程序中, 不仅有执行代码, 还有对象的大量的元数据. 通过称为反射reflection的特性可以实现序列化. 而在C++的编译出的程序中这些信息都没有.

因此, 我们需要自己实现序列化. 幸运的是, 还有一些库可以帮助我们. 例如Boost.Serializaton. 下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Address
{
string street;
string city;
int suit;
private:
friend class boost::serialization::access;
template<class Ar>
void serialize(Ar& ar, const unsigned int version)
{
ar & street;
ar & city;
ar & suite;
}
};

我们实际上是使用了&操作符.
它既可以用于saving也可以用于loading.

现在, 我们可以用相同的方式实现Contact类的序列化支持

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Contact
{
string name;
Address *address = nullptr;
private:
friend class boost::serialization::access;
template<class Ar>
void serialize(Ar& ar, const unsigned int version)
{
ar & name;
ar & address; // 不用写成 *address
}
};

注意, 我们这里不用写成ar & *address, boost足够智能, 它会自己识别指针并正确处理, 即使address是一个nullptr.

这样, 如果想以这种方式实现原型模式, 就需要为每个可能出现在对象中的类型实现其serialize()函数. 如果要做, 你需要做的就是定义通过序列化/反序列化实现对象的clone操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auto clone = [](const Contact& c)
{
//1. 序列化contact
ostingstream oss;
boost::archive::text_oarchive oa(oss);
oa << c;
string s = oss.str();

//2. 反序列化contact
istringstream iss(oss.str());
boost::archive::text_iarchive ia(iss);
Contact result;
ia >> result;
return result;
}

客户端代码就可以这样写:

1
2
Contact jane = clone(john);
jane.name = "Jane";

4.5 Prototype Factory 原型工厂

如果你已经定义好了一些要使用的原型对象, 你会把它们存到哪里? 或许是全局变量? 例如, 我们可以定义全局变量并保存在.h文件中:

1
2
Contact main{ "", new Address{ "123 East Dr", "London", 0 } };
Contact aux{ "", new Address{ "123B East Dr", "London", 0 } };

但是更好的做法是让一个单独的类来专门做这些事情.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct EmployeeFactory
{
static Contact main;
static Contact aux;

static unique_ptr<Contact>
NewMainOfficeEmployee(const string& name, int suite)
{
return NewPloyee(name, suite, main);
}

static unique_ptr<Contact>
NewAuxOfficeEmployee(const string& name, int s)
{
return NewEmployee(name, suit, aux);
}

private:
static unique_ptr<Contact>
NewEmployee(const string& name, int suite, Contact& proto)
{
auto result = make_unique<Contact>(proto);
result->name = name;
result->address->suite = suite;
return result;
}
}

前面的代码就可以这样写:

1
2
auto john = EmployeeFactory::NewAuxOfficeEmployee("John Doe", 123);
auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 125);

为什么要使用工厂, 主要是为了避免我们拷贝了原型之后又忘了做定制化操作.
而使用工厂, 并且将拷贝函数赋值函数等都设置为私有, 用户就没有其他的手段, 只能使用工厂来构造对象了.

4.6 总结

原型模式体现了对象**深拷贝(Deep copy)**的思想.

在C++中, 只有两种途径实现原型模式:

  • 写代码来正确实现对象的复制, 包括拷贝构造函数, 赋值操作符, 或单独的成员函数
  • 编写代码支持序列化/反序列化, 并利用这一机制来实现序列化之后立即反序列化. 这种做法会带来额外的计算成本, 严重依赖于拷贝行为的发生频率, 唯一的好处是能得到free的序列化/反序列化的能力.