access数据库做网站,建设哪里看额度,车险网站模版,建站收入C20设计模式 第 4 章 原型模式4.1 对象构建4.2 普通拷贝4.3 通过拷贝构造函数进行拷贝4.4 “虚”构造函数4.5 序列化4.6 原型工厂4.7 总结4.8 代码 第 4 章 原型模式
考虑一下我们日常使用的东西#xff0c;比如汽车或手机。它们并不是从零开始设计的#xff0c;相反#x… C20设计模式 第 4 章 原型模式4.1 对象构建4.2 普通拷贝4.3 通过拷贝构造函数进行拷贝4.4 “虚”构造函数4.5 序列化4.6 原型工厂4.7 总结4.8 代码 第 4 章 原型模式
考虑一下我们日常使用的东西比如汽车或手机。它们并不是从零开始设计的相反制造商会选择一个现有的设计方案对其作适当的改进使其外观区别于以往的设计然后淘汰老式的方案开始销售新产品。这是普遍存在的场景在软件世界中我们也会遇到类似的情形有时相比从零开始创建对象此时工厂和构造器可以发挥作用我们更希望使用预先构建好的对象或拷贝或基于此做一些自定义设计。 由此我们产生了一种想法即原型模式一个原型是指一个模型对象我们对其进行拷贝、自定义拷贝然后使用它们。原型模式的挑战实际上是拷贝部分其他一切都很简单。
4.1 对象构建
大多数对象通过构造函数进行构建。但是如果已经有一个完整配置的对象为ieshme不简单的拷贝该对象而非要重新创建一个相同的对象呢如果必须使用构造器模式来简化逐段构建对象的过程那么理解原型模式尤其重要。 我们先看一个简单但可以直接说明对象拷贝的示例
Contact john{John Doe, Address{123 East Dr , Londo, 10}};
Contact jane{Jane Doe, Address{123 East Dr , Londo, 11}};john和jane工作在同一栋建筑大楼的不同办公室。可能有许多人也在123 East Dr工作在构建对象时我们想避免重复对该地址信息做初始化。怎么做呢 原型模式与对象拷贝相关。当然我们没有通用的方法来拷贝对象但是可以选择一些可选的对象拷贝方法。
4.2 普通拷贝
如果曾在拷贝一个值和一个其所有成员都是通过值的方式来存储的对象那么拷贝毫无问题。例如在之前的示例中如果Contact和Address定义为 class Address{public:std::string street;std::string city;int suite;};class Contact{public:std::string name;Address address;};那么在使用赋值运算符进行拷贝时绝对不会有问题string类型拷贝为深拷贝 void testOrdinaryCopy() {// here is the prototypeContact worker{, {123 East Dr, London, 0}};// make a copy pf prototype and customize itContact john worker;john.name John Doe;john.address.suite 10;}但是在实际应用中这种按值存储和拷贝的方式较少见。在许多场景中通常将内部的Address对象作为指针或者引用例如 class Contact{public:std::string name;Address* address;~Contact() {delete address;}};现在有一个很棘手的问题因为代码Contact jane john将会拷贝地址指针所以john和jane以及其他每一个原型拷贝都会共享同一个地址这绝对不是我们想要的。 4.3 通过拷贝构造函数进行拷贝
避免拷贝指针的最简单的方法时确保对象的所有组成部分如上面的实例中的Contact和Address都完整定义了拷贝构造函数。例如如果使用原始指针保存地址即 class Contact{public:std::string name;Address* address;~Contact() {delete address;}};那么我们需要定义一个拷贝构造函数。在本示例中实际上有两种方法可以做到这一点。迎头而来的方法看起来像下面这种 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的street的成员是由街道名称、门牌号和一些附加信息组成的那该怎么版那时我们又会遇到同样的拷贝问题。 一种明智的做法是为Address定义拷贝构造函数。在本示例中Address的拷贝构造函数相当简单C string类型数据实现为深拷贝致使该拷贝构造函数非常简单 Address(std::string street, std::string city, int suite):street(street), city(city), suite(suite) {}现在我们可以重写Contact的构造函数中可以重用拷贝构造函数即 Contact(const Contact other):name(other.name), address(new Address(*other.address)) {}请注意ReSharper代码生成器在生成拷贝构造函数和移动构造函数的同时也会生成拷贝赋值函数。在本实例中拷贝赋值函数定义为 Contact operator(const Contact other) {if (this other) {return *this;}name other.name;address other.address;return *this;}【注】上述的拷贝赋值函数存在一定的问题当我们调用到赋值函数时并没有为address重新指定新的Address地址。会存在多个对象指向一块Address地址的问题这个可能不是我们所想见到的。 完成这些函数定义后我们可以像之前一样构造对象的原型然后重用它 void testCopyConstructor() {Contact worker{,new Address{123 East Dr, London, 0}};Contact john worker;john.name john;john.address-suite 10;}【注】在上述的测试代码中虽然使用了 “”但是并不会发生异常这和我们上一个注释说的就有点矛盾了是什么原因导致的呢 当对象赋值给另一个对象时C会根据情况调用拷贝构造函数或者拷贝赋值函数。如果在赋值操作时对象已经被初始化过那么会调用拷贝赋值函数。但如果在赋值操作时对象尚未初始化即对象已经存在那么会调用拷贝构造函数。这是因为赋值操作需要先创建对象然后再将值赋给已经存在的对象。因此这时会调用拷贝构造函数来初始化新对象。 所以这里虽然使用了 “” 但是其调用的是拷贝构造函数并不会调用拷贝赋值因此不会存在问题我们不妨把测试代码改写如下 void testCopyConstructor() {Contact worker{,new Address{123 East Dr, London, 0}};Contact john;john worker;john.name john;john.address-suite 10;}然后猜想下会发生什么异常呢 使用当前这种通过拷贝构造函数进行拷贝的方法是有效。使用这种方法唯一不足而且难以解决的问题是我们为此需要付出额外的工作以实现拷贝构造函数移动构造函数拷贝赋值函数等。诚然类似于ReSharper代码生成器一类的工具可以为大多数场景快速生成代码但会产生很多警告。例如我们编写如下的嗲吗并且忘记了提供Address类的拷贝赋值函数的实现会发生什么
Contact john worker;是的 程序仍然会通过编译。如果提供了拷贝构造函数会更好一些因为如果在没有定义构造函数的情况下尝试调用构造函数程序将会出错然而赋值运算符 “” 是普遍存在的。即使你没有为赋值运算符提供特殊的定义和实现。 还有一个问题假设使用类似二级指针的东西例如 void **或unique_str呢即使它们各有独特之处但此时像ReSharper和Clion这样的工具也不可能生成正确的代码所以使用工具为这些类型快速生成代码也许不是一个好主意。 4.4 “虚”构造函数
拷贝构造函数使用之处相当有限并且存在的一个问题是为了对变量的深度拷贝。我们需要知道变量具体是那种类型。假设ExtendedAddress类继承自Addressl类 class ExtendedAddress : public Address {public:std::string country;std::string postcode;ExtendedAddress(const std::string street, const std::string city, const int suite, const std::string country,const std::string postcode):Address(street, city, suite), country(country) {}ExtendedAddress* clone()override {return new ExtendedAddress(street, city, suite, country, postcode);}};若我们要拷贝一个存在多态性质的变量
ExtendedAddress ea ...;
Address a ea;
// dow do you deep-copy a ?这样的做法存在问题因为我们并不知道变量a的最终派生类型时是么。由于最终派生类引发的问题以及拷贝构造函数不能是虚函数。因此我们需要采用其他方法来创建对象的拷贝。 首先我们以Address对象为例引入一个虚函数clone()然后我们尝试 virtual Address clone() {return Address{street, city, suite};}不幸的是这并不能解决继承场景下的问题。请记住对于派生对象我们想返回的是ExtendedAddress类型。但上述代码展示的接口将返回类型固定为Address。我们需要是指针形式的多态因此再次尝试 virtual Address* clone() {return new Address{street, city, suite};}现在我们可以在派生类中做同样的事情只不过要提供对应的返回类型 ExtendedAddress* clone()override {return new ExtendedAddress(street, city, suite, country, postcode);}现在我们可以安全放心的调用clone()函数而不必担心对象由于继承体系被切割 void testVirtualConstructor() {std::cout __FUNCTION__ () begin.\n\n;ExtendedAddress ea{123 East Dr, London, 0, UK, SW101EG};Address a ea; //upcastauto cloned a.clone();printf(\n\nea: %s\n, typeid(ea).name()); // ExtendedAddressprintf(\n\na: %s\n, typeid(a).name()); // ExtendedAddressprintf(\n\ncloned: %s\n, typeid(cloned).name()); // Address*std::cout __FUNCTION__ () end.\n\n;}现在变量cloned的确是一个指向深度拷贝ExtendedAddress对象的指针了。当然这个指针的类型是Address*所以如果我们需要额外的成员则可以通过dynamic_cast进行转换或者调用某些虚函数。 如果处于某些原因我们想要使用拷贝构造函数则clone()接口可以简化为 ExtendedAddress* clone()override {return new ExtendedAddress(*this);}之后所有的工作都可以在拷贝构造函数中完成。 使用clone()方法的不足之处是编译器并不会检查整个继承体系每个类中实现的clone()方法并且也没有强行进行检查的方法。例如如果忘记在ExtendedAddress类中实现clone()方法示例代纳同样可以通过编译并且正常运行但当调用clone()方法是 clone()将构造一个Address而不是ExtendedAddress。
4.5 序列化
其他编程语言的设计者也遇到同样的问题即必须对整个对象显式定义拷贝操作并很快意识到类需要“普通可序列化”—默认情况下类应该可以直接写入字符串和流而不必使用任何额外的注释最多可能是一个或两个属性来指定类或其成员。 这与我们正在讨论的问题有关系吗当然有如果可以将类对象序列化到文件或内存中则可以再将其反序列化并保留包括其所依赖的对象在内的所有信息。这样我们就不需要在通过显式定义拷贝操作这种方式做处理获得一个在某个对象基础上的新对象。 遗憾的是与其他语言不同的是当提到序列化时C不提供免费的午餐。我们不能将复杂的对象序列化为文件。为什么不能在其他编程语言中编译的二进制文件不仅包括可执行代码还包括大量的元数据而序列化是通过一种反射的特性来实现的目前这个在C中是不支持的。 如果我们想要序列化那么就像显式拷贝操作一样我们需要自己实现它。幸运的是我们可以使用名为Boost.Serialization的现成的库来解决序列化的问题而不用费劲的处理和思考序列化std::string的方法。 【注】由于暂时不使用Boost库序列化就看到这块了后面有需要在补充… 4.6 原型工厂
如果我们预定义了要拷贝的对象那么我们会将它们保存在哪里全局变量中吗或许吧事实上假设我们公司有主办公室和备用办公室我们可以这样声明全局变量
Contact main{, new Address{123 East Dr, London, 0}};
Contact aux{, new Address{123B East Dr, London, 0}};我们可以将这些预定义的对象放在 Contact.h文件中, 任何使用Contact类的人都可以获取这些全局变量并进行拷贝。但更明智的方法是使用某种专用的类来存储原型并基于所谓的原型根据需要产生自定义拷贝。这将给我们带来更多的灵活性。例如我们可以定义工具函数产生适当初始化的unique_ptr:
class EmployeeFactory {static Contact main;static Contact aux;static std::unique_ptrContact NewEmployee(std::string name,int suite, Contact proto) {auto result std::make_uniqueContact(proto); //这里会调用拷贝构造result-name name;result-address-suite suite;return result;}public:static std::unique_ptrContact NewMainOfficeEmployee(std::string name , int suite) {return NewEmployee(name, suite, main);}static std::unique_ptrContact NewAuxMainOfficeEmployee(std::string name, int suite) {return NewEmployee(name, suite, aux);}};现在可以按如下方式使用 void testPrototypeFactory() {auto john EmployeeFactory::NewMainOfficeEmployee(John Doe, 123);auto jane EmployeeFactory::NewAuxMainOfficeEmployee(Jane Doe, 125);}为什么要使用工厂呢考虑这样一种场景我们从某个原型拷贝得到一个对象但忘记自定义该对象的某些属性此时该对象的某些本该有具体参数值的参数将为0或者空字符串。如果使用之前讨论的工厂我们可以将所有非完全初始化的构造函数声明为私有的并且将EmployeeFactory声明为friend class。现在客户将不再得到为完整构建的Contact对象。
4.7 总结
原型模式体现了对对象进行深度拷贝的概念因此不必每次都进行完全初始化而是可以获取一个预定义的对象拷贝它稍微修改它然后独立于原始的对象使用它。 在C中有两种方式实现原型模式的方法它们都需要手动操作 编写正确拷贝原始对象的代码也就是执行深度拷贝的代码。这项工作可以在拷贝构造函数 / 拷贝赋值运算符或者单独的成员函数中完成。 编写支持序列化 / 反序列化的代码使用序列化 / 反序列化机制在完成序列化后立即进行反序列化由此完成复制。该方法会引入额外的开销是否使用这种方法取决于具体使用场景下的拷贝频率。与使用拷贝构造函数相比这种方法的唯一优点是可以不受限制地使用序列化功能。
不论选择那种方法有些工作是必须完成的。如果决定采取上述两种方法的一种。则可采用一些代码生成工具比如类似于ReShareper和CLion的集成开发环境来辅助。 最后别忘了如果对所有数据采用按值存储的方式实际上并不会有问题只需要operator就够了。
4.8 代码
本章学习代码