本章涵盖了一些与C++API设计相关的设计模式和惯用法。
“设计模式(Design Pattern)”表示软件设计问题的一些通用解决方案。该术语来源于《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)
本书不会涵盖所有模式,只讨论一些和API设计有关的。
还会涉及一些C++的惯用法,它们并非是真正的通用设计模式,但却是C++API设计的重要技巧。
本章讨论的技巧如下:
Pimpl 意思是 Pointer to Implementation,指向实现的指针。
(Pimpl 不是严格意义上的“设计模式”,而是受制于C++语法特定限制的变通方案,可以看作是 桥接 设计模式的一种特例)
它主要解决的是C++语法中的一个问题:类的private成员其实只在内部使用,但是在定义时还需要写在.h文件中公开。因此,Pimpl 的做法是只在.h中定义一个指向实现的指针,那就可以将一些private成员放在.cpp中的实现中,这样就在.h文件中隐藏了private成员。
举例:

举例代码:
.h文件中:
class AutoTimer
{
public:
explicit AutoTimer();
~AutoTimer();
private:
class Impl;
Impl* mImpl;
}
随后,在.cpp中可以定义Impl类并实现具体的逻辑。
一个值得考虑的设计问题是:Impl类中放置多少逻辑?有以下选择:
Impl类中方法的简单包装)每种选择都适应于不同情况。一般情况下推荐 2 。
另外,使用 Pimpl 时需要注意:
Impl类中,否则公有类的子类无法继承。Impl类可能还需要一个公有类的指针以方便其调用公有类的方法。使用 Pimpl 的类无法进行默认的复制,因为复制出的对象会和原对象指向同一个Impl类变量。
这个问题有两种方法解决:
使用 Pimpl 时容易犯错的一点是:构造时忘记分配它,析构时忘记销毁它。
为此,可以采用 智能指针 或者 作用域指针。
Impl类实现发生变化,公用类的对象也不会改变二进制数据)Impl类对象会增加性能开销。mImpl->,如果Impl类需要调用公有类的方法也需要通过指针。Impl类的成员更改,公有类的const无法检查出。“单例”设计模式确保一个类仅存在一个实例,并提供唯一的全局访问点。
它可以看作是一种更优雅的全局变量,但是相比全局变量有一些优点:
其基本实现很简单,而本篇重点讨论的是:
其基本实现很简单:
class Singleton
{
public:
static Singleton &GetInstance();
};
Singleton &Singleton::GetInstance()
{
static Singleton instance;
return instance;
}
有一些做法可以增加健壮性:
GetInstance()返回引用而非指针:防止用户删除。上面的 GetInstance() 并非线程安全的。
常规的处理方式是加互斥锁。但这样会增加开销。
要优化此类激进的加锁行为,可以采用DCLP(Double Check Locking Pattern),即加互斥锁前先判断instance是否存在。
但是DCLP不能保证任何编译器和处理器下都能正常工作。
所以,也许你不应该尝试保证GetInstance()是线程安全的(毕竟对使用C++这样对并发缺乏内在支持的语言来说,实现线程安全总会遇到这些困难)。如果你真的需要它线程安全并且性能最高,可以考虑避免惰性实例化模型(即不要在需要他时再实例化),比如:
GetInstance()。GetInstance(),调用时可以加互斥锁。而GetInstance()的内部就不用加互斥锁了。初始化时传入需要的实例的指针,而不是内部再使用GetInstance()获得实例。
假设状态的初始化不需要控制,或者不需要使用单例对象存储,那么就可以使用“单一状态”,即:
类本身不保持是单例,但是其所有成员(或者说“状态”)都是static。
《设计模式》作者指出,单例有可能导致拙劣的设计。使用时候需要思考,“单例”是否真的是正确的模式?
需求是会变的,未来有些对象可能会需要支持多个实例。
因此,需要尽早考虑引入 “会话(session)” 或 “上下文(context)” 的概念。这是在强调:使用单一的实例维护所有相关的状态,而非使用多个单例。
工厂模式是关于创建的设计模式,本质上是构造函数的泛化,可以回避C++构造函数的限制:
从使用层面上看,工厂方法仅是一个普通的方法,调用时返回对应类的实例:
class RendererFactory
{
public:
IRenderer* CreateRenderer(string type)
}
但这里的 IRenderer 是一个抽象基类(Abstract Base Class,简称ABC)。抽象基类是包含纯虚函数的类,不能被实例化。(另外要注意,抽象基类的析构函数需要声明为虚的)。
这样,用户可以使用参数动态决定要创建的类型。
另外作为扩展,可以让工厂类提供注册函数,这样用户可以自己添加新的类型
static void RegisterRender(string type, CreateCallback cb);
基于另一组API来包装接口是一项常见的API设计任务。比如,你在维护一个遗留的代码库,相比重构代码,你更愿意封装一套新的,更简洁的API,以隐藏所有的底层遗留代码。
下面,按照包装器层和原始接口的差异程度递增地划分:
一对一地,将函数调用转发到具有相同形式的另一个接口。
案例:
一对一地,将接口转换为一个兼容但是不完全相同的另一个接口。
优点:
外观模式能够为一组类提供简化的接口。它实际上定义了一个更高层次的接口,使得底层类更易于使用且对用户隐藏。
(一个例子:用一个“酒店助手类”简化了“预定房间”、“预定晚餐”、“预定出租车”等事务。)
用途:
观察者模式为了解决这样的问题:
实现复杂的任务通常需要多个对象一起合作完成。为了让A可以调用B,较为简单的方法就是A.cpp包含B.h,但是这样产生了编译时依赖,迫使想要复用A时也必须引入B。
观察者模式就是 “发布/订阅” 范式的一个具体实例。
实现观察者模式的典型做法是引入两个概念:
代码上,Subject仅知道Observer接口类即IObserver,他将维护IObserver列表
class IObserver
{
virtual void Update() = 0;
}
class ISubject
{
std::vector<IObserver*> ObserverList
}
随后,观察者通过继承IObserver来实现观察者对象。
然后,在使用时,Subject就可以订阅(Subscribe)若干Observer,并在需要的时候通知(Notify)它们。
这样,Subject和Observer就没有编译时依赖关系,它们的关系是运行时动态创建的。