C++ 编程思想 第二卷 10章 设计模式

代理(Proxy)模式和状态(State)模式都提供一个代理(Surrogate)类。代码与代理类打交道,而做实际工作的类隐藏在代理类背后。当调用代理类中的一一个函数时,代理类仅转而去调用实现类中相应的函数。

这两种模式是如此相似,从结构上看,可以认为代理模式只是状态模式的一个特例。设想将这两者合理地混合在一起组成一个称为代理(Surogate)设计模式,这肯定是一个很具有诱惑力的想法,但是这两个模式的内涵 (intent)是不一样的。这祥做很容易陷人”如果结构相同模式就相同”的思想误区。必须始终关注模式的内涵,从而明确它的功能到底是什么。

基本思想很简单:

  • 代理(Surogate)类派生自个基类,由平行地派生自同一个基类的一个或多个类提供实际的实现

当一个代理对象被创建的时候,一个实现对象就分配给了它,代理对象就将函数调用发给实现对象。

从结构上来看。代理模式和状态模式的区别很简单;代理模式只有一个实现类,而状态模式有多个(一个以上)实现。(在GoF中)认为这两种设让模式的应用也不同;代理模式控制对其实现类的访问,而状态模式动态地改变其实现类。然而,如果广义理解”控制对实现类的访问”,则这两个模式似乎是一个连续体的两部分。

代理模式: 作为其他对象的前端

状态模式: 改变对象的行为

C++ API设计 第3章 模式 - API包装器模式

编写基于另一组类的包装器接口是一项常见的API设计任务。
例如

  • 你的工作可能是维护一个大型的遗留代码库,相比重构所有代码,你更愿意设计一个新的、更简洁的API。以隐藏所有的底层遗留代码(Feathers,2004)
  • 或者你可能已经编写了一个C++API,后来需要给特定客户提供纯C接口
  • 或者你的API用到了一个第三方依赖库,你想让客户直接使用此库,但是又不想将此库直接暴露给客户。

创建包装器API的潜在副作用是影响性能,这主要因为额外增加的一级间接寻址以及存储包装层次状态带来的开销。但就上面提到的那些情况而言,这么做可以创建质量更高的、更有针对性的API,是物有所值的。

结构化设计模式可以处理接口包装任务.
按照包装器层和原始接口的差异递增程度划分,它们依次是∶ 代理、适配器和外观。

代理模式

Dingtalk_20211130105647.jpg
代理设计模式另一个类提供了一对一的转发接口

  • 调用代理类的FunctionA()将导致调用原始类中的FunctionA()。也就是说,代理类和原始类有相同的接口。它可以被认为是一个单一组件包装器(single-component wrapper)是Lakos提出的术语,意为代理API中的一个类映射到原始API中的一个类。

此模式通常的实现是,代理类中存储原始类的副本,但更可能是指向原始类的指针,然后代理类中的方法将重定向到原始类对象中的同名方法。这种技巧的一个缺点是需要再次暴露原始对象的函数。此过程本质上就是复制代码。因此,在改变原始对象时,这种方案需要维护代理接口的完整性。下面的代码是此技巧的一个简单示例。注意,将复制构造函数和赋值操作符声明为私有成员函数是为了阻止客户复制此对象。你也可以提供这些函数的显式实现,然后允许复制操作,

  1. // original.h
  2. #ifndef ORIGINAL_H
  3. #define ORIGINAL_H
  4. ///
  5. /// An Original class wrapped by Proxy.
  6. ///
  7. class Original
  8. {
  9. public:
  10. bool DoSomething(int value);
  11. };
  12. #endif
  1. // original.cpp
  2. #include "original.h"
  3. #include <iostream>
  4. using std::cout;
  5. using std::endl;
  6. bool Original::DoSomething(int value)
  7. {
  8. cout << "In Original::DoSomething with value " << value << endl;
  9. return true;
  10. }
  1. // proxy.h
  2. #ifndef PROXY_H
  3. #define PROXY_H
  4. class Original;
  5. ///
  6. /// A Proxy class that wraps access to an Original class.
  7. ///
  8. class Proxy
  9. {
  10. public:
  11. Proxy();
  12. ~Proxy();
  13. /// Call through to the Original::DoSomething() function
  14. bool DoSomething(int value);
  15. private:
  16. // prevent copying of this class because we had a pointer data member
  17. Proxy(const Proxy &);
  18. const Proxy &operator =(const Proxy &);
  19. Original *mOrig;
  20. };
  21. #endif
  1. // proxy.cpp
  2. #include "proxy.h"
  3. #include "original.h"
  4. #include <iostream>
  5. using std::cout;
  6. using std::endl;
  7. Proxy::Proxy()
  8. : mOrig(new Original)
  9. {
  10. cout << "Allocated new Original object inside Proxy" << endl;
  11. }
  12. Proxy::~Proxy()
  13. {
  14. delete mOrig;
  15. cout << "Destroyed Original object inside Proxy" << endl;
  16. }
  17. bool Proxy::DoSomething(int value)
  18. {
  19. cout << "About to call Original::DoSomething from Proxy" << endl;
  20. return mOrig->DoSomething(value);
  21. }
  1. // main.cpp
  2. #include "proxy.h"
  3. int main(int, char **)
  4. {
  5. Proxy proxy;
  6. proxy.DoSomething(42);
  7. return 0;
  8. }

另一个方案是在此方案的基础上增加代理和原始API共享的虚接口,这样做是为了更好地保持这两个API的同步、这么做的前提是你能够修改原始API。下面的代码演示了此方案

  1. // original.h
  2. #ifndef ORIGINAL_H
  3. #define ORIGINAL_H
  4. #include "ioriginal.h"
  5. ///
  6. /// An Original class wrapped by Proxy.
  7. ///
  8. class Original : public IOriginal
  9. {
  10. public:
  11. bool DoSomething(int value);
  12. };
  13. #endif
  1. // original.cpp
  2. #include "original.h"
  3. #include <iostream>
  4. using std::cout;
  5. using std::endl;
  6. bool Original::DoSomething(int value)
  7. {
  8. cout << "In Original::DoSomething with value " << value << endl;
  9. return true;
  10. }
  1. // proxy.h
  2. #ifndef PROXY_H
  3. #define PROXY_H
  4. #include "ioriginal.h"
  5. class Original;
  6. ///
  7. /// A Proxy class that wraps access to an Original class.
  8. ///
  9. class Proxy : public IOriginal
  10. {
  11. public:
  12. Proxy();
  13. ~Proxy();
  14. /// Call through to the Original::DoSomething() function
  15. bool DoSomething(int value);
  16. private:
  17. // prevent copying of this class because we had a pointer data member
  18. Proxy(const Proxy &);
  19. const Proxy &operator =(const Proxy &);
  20. Original *mOrig;
  21. };
  22. #endif
  1. // proxy.cpp
  2. #include "proxy.h"
  3. #include "original.h"
  4. #include <iostream>
  5. using std::cout;
  6. using std::endl;
  7. Proxy::Proxy()
  8. : mOrig(new Original)
  9. {
  10. cout << "Allocated new Original object inside Proxy" << endl;
  11. }
  12. Proxy::~Proxy()
  13. {
  14. delete mOrig;
  15. cout << "Destroyed Original object inside Proxy" << endl;
  16. }
  17. bool Proxy::DoSomething(int value)
  18. {
  19. cout << "About to call Original::DoSomething from Proxy" << endl;
  20. return mOrig->DoSomething(value);
  21. }
  1. // main.cpp
  2. #include "proxy.h"
  3. int main(int, char **)
  4. {
  5. // create a proxy instance and call one of its functions
  6. Proxy proxy;
  7. proxy.DoSomething(42);
  8. return 0;
  9. }

用代理模式的一些案例

  • 实现原始对象的惰性实例

在此情况下,直到方法被调用时,Original对象才真正实例化。如果实例化Original对象是一个重量型操作,那么这样做就很有益了,因为这样做可以推迟对象创建时间,且只在对象真正被需要时才会创建。

  • 实现对Original对象的访问

例如要在ProxyOriginal对象之间插入权限层,以确保当用户获得适当的授权后,只能调用Original对象上的特定方法。

  • 支持调试或”演习”模式

支持在Proxy方法中插入调试语句记录所有对Original对象的调用,或者使用一个标志以”演习”(dry run)模式调用Proxy,以禁止调用特定的Original方法;例如,禁止将对象状态写入磁盘。

  • 保证Original类线程安全

通过给非线程安全的方法添加互斥锁实现线程安全。这可能不是保证基础代码线程安全最有效的方式,但是如果不能修改Original类,这也是一个权宜之计。

  • 支持资源共享

可以让多个Proxy对象共享相同的OriginaI基础类。例如,这可以用于实现引用计数或写时复制(copy-on-write)语义。这种用法实际上是另一个设计模式,称为享元模式(Flyweight ),在此模式中多个对象共享相同的基础数据以减小内存占用。

  • 应对Original类将来被修改的情况

在此情况下,如果你预期依赖库未来会改变,就可以为该API创建一个代理包装器直接模拟当前的行为。当库发生改变时,可以通过代理对象预留老接口,改变代理类的底层实现就可以使用新的库方法。就此点而言,这已不是代理对象,而是一个适配器对象,它是我们下面马上要讲解的设计模式。

适配器模式

Dingtalk_20211130112943.jpg
适配器设计模式将一个类的接口转换为一个兼容的但不相同的接口。与代理模式的相似之处是,适配器设计模式也是一个单一组件包装器,但适配器类和原始类的接口可以不相同。

此模式能够为现有API暴露一个不同的接口,进而和其他代码集成。利用代理模式,待处理的两个接口可以来自不同的类。

  • 例如一个几何包,支持定义一系列的基本图形。某些方法的参数顺序可能与你使用的API不同,或者指定的坐标系统不同,或者使用习惯不同
  • 例如(圆心,半径)与(左下,右上),或者方法名可能不遵循你的API命名约定

这时就可以使用适配器类将此接口转换为与你的API的兼容形式

  1. // adapter.h
  2. #ifndef ADAPTER_H
  3. #define ADAPTER_H
  4. // forward declaration for the object wrapped by Adapter
  5. class Original;
  6. ///
  7. /// An Adapter that wraps access to an Original object.
  8. ///
  9. class Adapter
  10. {
  11. public:
  12. Adapter();
  13. ~Adapter();
  14. /// Call through to Original::DoSomething()
  15. bool DoSomething(int value);
  16. private:
  17. // prevent copying of this class because we had a pointer data member
  18. Adapter(const Adapter &);
  19. const Adapter &operator =(const Adapter &);
  20. Original *mOrig;
  21. };
  22. #endif
  1. // adapter.cpp
  2. #include "adapter.h"
  3. #include "original.h"
  4. #include <iostream>
  5. using std::cout;
  6. using std::endl;
  7. Adapter::Adapter()
  8. : mOrig(new Original)
  9. {
  10. cout << "Allocated new Original object inside Adapter" << endl;
  11. }
  12. Adapter::~Adapter()
  13. {
  14. delete mOrig;
  15. cout << "Destroyed Original object inside Adapter" << endl;
  16. }
  17. bool Adapter::DoSomething(int value)
  18. {
  19. cout << "About to call Original::DoOperation from Adapter::DoSomething" << endl;
  20. mOrig->DoOperation(value, true);
  21. return true;
  22. }
  1. // adapter.h
  2. #ifndef ADAPTER_H
  3. #define ADAPTER_H
  4. // forward declaration for the object wrapped by Adapter
  5. class Original;
  6. ///
  7. /// An Adapter that wraps access to an Original object.
  8. ///
  9. class Adapter
  10. {
  11. public:
  12. Adapter();
  13. ~Adapter();
  14. /// Call through to Original::DoSomething()
  15. bool DoSomething(int value);
  16. private:
  17. // prevent copying of this class because we had a pointer data member
  18. Adapter(const Adapter &);
  19. const Adapter &operator =(const Adapter &);
  20. Original *mOrig;
  21. };
  22. #endif
  1. // adapter.cpp
  2. #include "adapter.h"
  3. #include "original.h"
  4. #include <iostream>
  5. using std::cout;
  6. using std::endl;
  7. Adapter::Adapter()
  8. : mOrig(new Original)
  9. {
  10. cout << "Allocated new Original object inside Adapter" << endl;
  11. }
  12. Adapter::~Adapter()
  13. {
  14. delete mOrig;
  15. cout << "Destroyed Original object inside Adapter" << endl;
  16. }
  17. bool Adapter::DoSomething(int value)
  18. {
  19. cout << "About to call Original::DoOperation from Adapter::DoSomething" << endl;
  20. mOrig->DoOperation(value, true);
  21. return true;
  22. }
  1. // main.cpp
  2. #include "adapter.h"
  3. int main(int, char **)
  4. {
  5. // create an adapter object and call one of its functions
  6. Adapter adapter;
  7. adapter.DoSomething(42);
  8. return 0;
  9. }

需要注意的是,适配器可以用”组合”(前面的例子提到过)或者”继承”来实现。这两种类型分别称为对象适配器和类适配器
如果用继承实现,RectangleAdapter则继承Rectangle基类。如果还要在适配器API中暴露Rectangle的接口,则可以使用公有继承;但我推荐使用私有继承,因为这样仅需声明新的接口为公有即可。

对于API设计而言,适配器模式的优点如下所述。

  • 强制API始终保持一致性

一致性是优质API的重要特征之一。如果有多个不同的类,且这些类的接口风格各不相同,那么使用适活配器模式能够整合这些类。并为它们提供一致的接口。这样做的好处是API更统一、更易于使用。

  • 包装API的依赖库

例如你的API提供加载PNG图像的功能。你使用libpng库实现此功能,但是不想将libpng调用直接暴露给API用户,带来的好处是使API一致且统一,而且可以预防libpng将来发生变化。

  • 转换数据类型

假设你有一个MapP1ot API,它支持在2D地图上绘制地理坐标。MapPlot只接受经度和纬度组成的参数对(使用WGS84基准面),且经度和纬度都使用双精度类型。但你的API使用GeoCoordinate类型表示几种坐标系统中的坐标,比如横轴墨卡托(Universal Transverse Mercator)和兰伯特正形圆锥投影(Lambert Conformal Conic )。这时你可以编写一个适配器,接收GeoCoordinate对象作为输入参数,在适配器内部将其转换为地理坐标(经度,纬度),必要的时再将这两个双精度值传递给MapPlot API

  • 为API暴露一个不同的调用约定

假如你编写了一个纯C API,要为C++用户提供一个面向对象的版本。那么你可以创建一个适配器类,将C调用包装在C++类中。这种实现是否能被归为适配器模式尚存争议,因为设计模式主要是针对面向对象的系统而言。但如果你愿意更灵活地理解此术语,就会发现它们在概念上是完全相同的。
下面的代码给出了纯C API的C++适配器示例。

  1. class CppAdapter
  2. {
  3. public:
  4. CppAdapter()
  5. {
  6. mHandle = create_object();
  7. }
  8. ~CppAdapter()
  9. {
  10. destrop_object(mHandle);
  11. mhandle = NULL;
  12. }
  13. void DoSomething(int value)
  14. {
  15. object_do_something(mHandle, value);
  16. }
  17. private:
  18. CppAdapter(const CppAdapter&);
  19. const CppAdapter &operator=(const CppAdapter&);
  20. CHandle *mHandle;
  21. };

外观模式

Dingtalk_20211130144848.jpg
外观模式能够为一组类提供简化的接口。它实际上定义了一个更高层次的接口,以使得底层子系统更易于使用。依据Lakos的分类方法,外观模式是多组件包装器的一个示例(Lakos,1996)。
外观模式和适配器模式的区别是,外观模式简化了类的结构,而适配器模式仍然保持相同的类结构。

随着API的扩展,API接口的使用复杂度也日益增加。外观模式是一种将API解构为子系统,从而降低这种复杂度的方法。目的在于让更多的客户用上易用的API。外观模式一方面提供了改进的API接口,另一方面仍能支持对底层子系统的访问。这与前一章讨论的便捷API的概念相同,即添加额外的类以提供聚合功能,使得简单任务更简单。
外观模式还可以将底层子系统和公有接口完全分离,进而底层类不再可访问。我们通常称之为”封装的外观模式”。

  1. // original.h
  2. //
  3. // One class in the sub-system wrapped by Facade.
  4. //
  5. class Original1
  6. {
  7. public:
  8. int DoOperation1();
  9. };
  10. //
  11. // Another class in the sub-system wrapped by Facade.
  12. //
  13. class Original2
  14. {
  15. public:
  16. bool DoOperation2(int value);
  17. };
  18. #endif
  1. // original.cpp
  2. #include "original.h"
  3. #include <iostream>
  4. using std::cout;
  5. using std::endl;
  6. int Original1::DoOperation1()
  7. {
  8. cout << "In Original::DoOperation1" << endl;
  9. return 42;
  10. }
  11. bool Original2::DoOperation2(int value)
  12. {
  13. cout << "In Original::DoOperation2 with value " << value << endl;
  14. return true;
  15. }
  1. // facade.h
  2. #define FACADE_H
  3. ///
  4. /// An facade that wraps access to a sub-system.
  5. ///
  6. class Facade
  7. {
  8. public:
  9. Facade();
  10. ~Facade();
  11. /// Call various functions in the sub-system to aggregate its functionality
  12. bool DoSomething();
  13. private:
  14. Facade(const Facade &);
  15. const Facade &operator =(const Facade &);
  16. class Impl;
  17. Impl *mImpl;
  18. };
  19. #endif
  1. // facade.cpp
  2. #include "facade.h"
  3. #include "original.h"
  4. #include <iostream>
  5. using std::cout;
  6. using std::endl;
  7. class Facade::Impl
  8. {
  9. public:
  10. Impl() :
  11. mOriginal1(NULL),
  12. mOriginal2(NULL)
  13. {
  14. }
  15. ~Impl()
  16. {
  17. delete mOriginal1;
  18. delete mOriginal2;
  19. cout << "Destroyed Original objects inside Facade" << endl;
  20. }
  21. Original1 *GetOriginal1()
  22. {
  23. if (! mOriginal1)
  24. {
  25. cout << "Lazily allocating Original1" << endl;
  26. mOriginal1 = new Original1;
  27. }
  28. return mOriginal1;
  29. }
  30. Original2 *GetOriginal2()
  31. {
  32. if (! mOriginal2)
  33. {
  34. cout << "Lazily allocating Original2" << endl;
  35. mOriginal2 = new Original2;
  36. }
  37. return mOriginal2;
  38. }
  39. private:
  40. Original1 *mOriginal1;
  41. Original2 *mOriginal2;
  42. };
  43. Facade::Facade()
  44. : mImpl(new Facade::Impl)
  45. {
  46. }
  47. Facade::~Facade()
  48. {
  49. delete mImpl;
  50. }
  51. bool Facade::DoSomething()
  52. {
  53. cout << "About to call routines in Original1 and Original2" << endl;
  54. int result = mImpl->GetOriginal1()->DoOperation1();
  55. if (result < 100)
  56. return mImpl->GetOriginal2()->DoOperation2(result);
  57. return false;
  58. }
  1. // main.cpp
  2. #include "facade.h"
  3. int main(int, char **)
  4. {
  5. // create the facade object and call one of its functions
  6. Facade facade;
  7. facade.DoSomething();
  8. return 0;
  9. }

用一个示例阐述此模式

  • 假设你在度假并入住了一家酒店。你计划先用晚餐然后去看演出。你需要先给餐厅打电话预定晚餐,打电话给剧院预定座位,可能还需要安排出租车来接你。
  • 在C++中,可以将这3件事表示为3个独立的对象,并逐一处理每个对象。 ```cpp class Taxi { public: bool BookTaxi(int npeople, time_t pickup_time); };

class Restautrant { public: bool ReserveTable(int npeople, time_t arrival_time); };

class Theater { public: time_t GetShowTime(); bool ReserveSeats(int npeople, int tier); };

  1. 这回假设你入住的是一家高档酒店,酒店的礼宾部能够帮助你完成所有的事情。实际上,礼宾部会首先查出演出时间,然后根据其掌握的当地情况,计算出合适的晚餐时间,并在最佳时间为你预定出租车。<br />将此转换为C++设计的术语就是,你只需处理一个对象,且此对象的接口比上一个方案的接口更简单。
  2. ```cpp
  3. class ConciergeFacade
  4. {
  5. public:
  6. enum ERestaurant
  7. {
  8. RESTAURANT_YES,
  9. RESTAURANT_NO
  10. };
  11. enum ETaxi
  12. {
  13. TAXI_YES,
  14. TAXI_NO
  15. };
  16. time_t BookShow(int npeople, ERestaurant addRestaurant, ETaxi addtaxi);
  17. };

就API设计而言,外观模式有多种用途

  • 隐藏遗留代码

在工作中我们经常需要处理一些遗留系统,它们陈旧、落后、脆弱且不再提供一致的对象模型。在这种情况下,简化工作的方法是基于原有代码创建一组设计良好的API。这样,所有的新代码就能使用这些新的API。在所有客户都更新了新的API后,遗留代码就隐藏在了新的接口之下。(这等于实现了一个封装外观模式。)

  • 创建便捷API

前一章讨论过,通用、灵活的API功能强大,而简单且易于使用的API能够使简单的任务更简单,但这二者通常不可兼得。使用外观模式可以让二者并存,从而解决此问题。本质上讲,便捷API就是一个外观模式。我们曾使用过OpenGL的示例,其中GL库提供了底层的基础例程,GLU库则基于GL库提供了高层次的且易于使用的接口。

  • 支持简化功能或者替代功能的API

抽象出对底层子系统的访问之后,就能替换某个子系统,而且不会影响客户代码。在占位子系统中,它能够置换子系统以支持API的演示版本或测试版本。它也允许置换不同的功能,比如为游戏使用不同的3D渲染引擎或使用不同的图像读取库。一个真实的的例子是,Second Life Viewer可以使用专有的KDUJPEG-2000解码器实现,而开源的Second Life Viewer则是基于性能略差的OpenJPEG库实现的。