0%

C++设计模式(一)基于Policy的class设计

原图

概述

这一章将介绍所谓 policies和 policy classes它们是一种重要的 classes 设计技术,能够增加程序库的弹性并提高复用性,这正是Loki的目标所在。简言之,具备复杂功能的 policy based class由许多小型 classes(称为 policies)组成。每一个这样的小型 class 都只负责单纯如行为或结构的某一方面( behavioral or structural aspect)。一如名称所示,一个 policy会针对特定主题建立个接口。在“遵循 policy接口”的前提下,你可以用任何适当的方法来实作 policies。

由于你可以混合并匹配各种 policies,所以藉由小量核心基础组件( core elementary components)的组合,你可完成一个“行为集”( behaviors set)。

软件设计的多样性(Multiplicity)

让我们考虑一个简単的入门级程序维型:-个 Smart Pointer(智能指针)。这种 class可被用于单线程或多线程之中,可以运用不同的 ownership(拥有权)策略,可以在安全与速度之间协调,可以支持或不支持“自动转为内部指针以上这些特性都可以被自由组合起来而所谓解答,就是最适合你的应用程序的那个方案。

全功能型(Do-It-All)接口的失败

如果程序库将不同的设计实作为各个小型 classes,每个 class代表一个特定的罐装解法,如何?例如在 smart pointer例中你会看到 Singlethreaded Smartptr, Multithreadedsmartptr、Refcountedsmartptr、 Reflinkedsmartptr 等等。 这种做法的问题是会产生大量设计组合。例如上述提到的四个 classes必然导致诸如 singlethreadedrefcountedsmartptr这样的组合。如果再加一个设计选项(例如支持型別转换),会产生更多澘在组合。这最终会让程序库实作者和使用者受不了。很明显地,这并不是个好方法。面对这么多的潜在组合(近乎指数爬升),千万別企图使用暴力枚举法。 这样的程序库不只造成大量的智力负荷,也是极端的严苛而死板。一点点轻微不可测的定制行为(例如试着以一个特定值为预先构造好的 smart pointers设初值)都会导致整个精心制作的 library classes没有用处。 设计是为了厉行 constraints(约束条件、规范)。因此,以设计为目标的程序库必须帮助使用者精巧完成设计,以实现使用者自己的 constraints,而不是实现预先定义好的 constraints。罐装设计不适用于以设计为目标用途的程序库,就像术数字不适用于ー般代码一样。当然啦,一些“最普遍的、受推荐的”罐装解法将受到大众欢迎只要客端程序员必要时能够改变它们。

多重继承( Multiple Inheritance)是救世主?

藉由多重继承机制来组合多项功能,会产生如下问题:

  • 关于技术( Mechanics)。目前并没有一成不变即可套用的代码,可以在某种受控情况下将继承而来的 classes组合( assemble)起来。唯一可组合 Basesmartptr, Multithreaded和Refcounted的工具是语言提供的“多重继承”机制:仅仅只是将被组合的 base classes结合在一起并建立一组用来访问其成员的简单规则。除非情况极为单纯,否则结果难以让人接受。大多数时候你得小心协调继承而来的 classes的运转,让它们得到所需的行为。
  • 关于型别信息( Type information)。 Based classes并没有足够的型信息来继续完成它们的工作。例如,想象一下,你正试着藉由继承一个 Deepcopy class来为你的 smart pointer实作出深层拷贝( deep copy)。但 Deepcopy应该具有怎样的接口呢?它必须产生一个对象,而其型别目前未知。
  • 关于状态处理( State manipulation) base classes实作之各种行为必须操作相同的 state(译注:意指数据)。这意味着他们必须虚继承一个持有该 state的 base class由于总是由user classes继承 library classes(而非反向),这会使设计更加复杂而且变得更没有弹性。

虽然本质上是组合( combinatorial),但多重继承无法单独解决设计时的多样性选择。

Templates带来曙光

templates是一种很适合“组合各种行为”的机制,主要因为它们是“依赖使用者提供的型别信息”并且“在编译期才产生”的代码。 和一般的class不同, class templates可以以不同的方式定制。如果想要针对特定情况来设计class你可以在你的 class template中特化其成员函数来因应。举个例子,如果有一个 Smartart你可以针对 SmartPtr特化其任何成员函数,这可以为你在设计特定行为时提供良好粒度(granular)。 犹有进者,对于带有多个参数的 class templates你可以采用 partial template specialization(偏特化)。它可以让你根据部分参数来特化一个 class template。例如,下面是个 template定义:

1
template <class T, Class U> class Smartptr {...};

你可以令 SmartPtr<T,U>针对 widget及其他任意型别加以特化,定义如下:

1
template <class U> class SmartPtr<Widget, U> {...};

template的编译期特性以及“可互相组合”特性,使它在设计期非常引人注目。然而一旦你开始尝试实作这些设计,你会遭遇一些不是那么浅白的问题.

  • 你无法特化结构。单单使用 templates,你无法特化“ class 的结构”(我的意思是其数据成员),你只能特化其成员函数。
  • 成员函数的特化并不能“依理扩张”。你可以对“单一 template参数”的 class template特化其成员函数,却无法对着“多个 template参数”的 class template特化其个别成员函数。例如:
1
2
3
4
5
6
7
8
9
10
template <class T> class Widget
{
void fun() {}
}

//Okay: specialization of a member function of widget
template <> void Widget<char>:: fun()
{
void fun() {}
}

但是,下面是我被告知的错误。

1
2
3
4
5
6
7
8
9
10
template<class T, class U> class Gadget
{
void fun() {}
}

//Error! cannot partially specialize a member function of Gadget
template<class U> void Gadget<char,U>::fun()
{
..specialized implementation
}

仅对一个成员函数进行部分特化是不可能的,而必须对整个类进行部分特化。这就是C++中的工作方式。

原因是您不能拥有部分专门的功能,而成员功能本身就是功能。通过部分地专用整个类,成员函数将“看起来”像具有较少类型的模板(在该部分专用类中)。

  • 程序库撰写者不能够提供多笔缺省值。理想情况下 class template 的作者可以对每个成员函数提供一份缺省实作品,却不能对同一个成员函数提供多份缺省实作品。

现在让我们比较一下多重继承和 templates之间的缺点。有趣的是两者互补。多重继承欠缺技术( mechanics), templates有丰富的技术。多重继承缺乏型别信息,而那东西在 templates里大量存在。 Templates的特化无法扩张( scales),多重继承却很容易扩张。你只能为 template成员函数写一份缺省版本,但你可以写数量无限的 base classes 根据以上分析,如果我们将 templates和多重继承组合起来,将会产生非常具弹性的设备( device),应该很适合用来产生程序库中的“设计元素”( design elements)。

Policies 和 Policy Classes

Policies和 Policy Classes有助于我们设计出安全、有效率且具高度弹性的“设计元素”。所谓 policy,乃用来定义一个 class 或 class template的接口,该接口由下列项目之一或全部组成:内隐型别定义( inner type definition)、成员函数和成员变量

Policies也被其他人用于 traits( Alexandrescu2000a),不同的是后者比较重视行为而非型别。Policies也让人联想到设计模式 Strategy( Gamma et a.195),只不过 policies吃紧于编译期(所谓compile-time bound)。 举个例子,让我们定义一个 policy用以生成对象: Creator policy是一个带有型别T的class template,它必须提供一个名为 Create的函数给外界使用,此函数不接受引数,传回-个 pointer-to-T。就语义而言,每当 Create()被调用就必须传回一个指针,指向新生的T对象。至于对象的精确生成模式( creation mode),留给 policy实作品作为回旋余地。 让我们来定义一个可实作出 Creator policy的 class产生对象的可行办法之一就是表达式new另一个办法是以malloc()加上placement new操作符( Meyers1998b)。此外,还可以采用复制( cloning)方式来产生新对象。下面是三种做法的实呈现:

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
28
29
30
31
32
33
34
35
template <class T>
struct OpNewCreator
{
static T* Create()
{
return new T;
}
};

template <class T>
struct MallocCreator
{
static T* Create()
{
void* buf = std::malloc(sizeof(T));
if (!buf) return 0;
return new(buf) T;
}
};

template <class T>
struct PrototypeCreator
{
PrototypeCreator(T* pObj = 0)
:pPrototype_(pObj)
{}
T* Create()
{
return pPrototype_ ? pPrototype_->Clone() : 0;
}
T* GetPrototype() { return pPrototype_; }
void SetPrototype(T* pObj) { pPrototype_ = pObj; }
private:
T* pPrototype_;
};

任何一个 policy都可以有无限多份实作品。实作出 policy者使称为 policy classes,这个东西并不意图被单独使用,它们主要用于继承或被内含于其他 classes。 这里有一个重要观念: policies接口和一般传统的 classes接口(纯虚函数集)不同,它比较松散,为policy是语法导向( syntax oriented)而非标记导向( signature oriented)換句话说, Creator明确定义的是“怎样的语法构造符合其所规范的class”,而非“必须实作出哪些函数”。例如Creator policy并没有规范 Create()必须是 static还是 virtual,它只要求class必须定义出Create()。此外, Creator也只规定 create)应该(但非必须)传回一个指向新对象的指针因此 Create()也许会传回0或丢出异常一这都极有可能。 面对一个 policy,你可以实作出数个 policy classes它们全都必须遵守 policy所定义的接口。稍后你会看到一个例子,使用者选择了一个 policy并应用到较大结构中。 先前定义的三个 policy classes各有不同的实作方式,甚至连接也有些不同(例如 Prototypecreator多了两个函数 GetPrototype()和 Setprototype()。尽管如此,它们全都定义 Create()并带有必要的返回型別,所以它们都符合 Creator policy 现在让我们看看如何设计一个 class得以利用 Creator policy,它以复合或继承的方式使用先前所定义的三个 classes之,如:

1
2
3
//Library code
template <class CreationPolicy>
class WidgetManager: public CreationPolicy

如果 class采用一个或多个 policies,我们称其为 hosts或 host classes。上例的 WidgetManager使是“采用了个 policy'”的 host class. Hosts负责把 policies提供的结构和行为组成一个更复杂的结构和行为。 当客户端将 WidgetManager template具现化( Instantiating)时,必须传进一个他所期望的 policy:

1
2
// Application code
typedef WidgetManager< Opnewcreator<Widget> MyWidgetMgr;

让我们分析整个来龙去脉。无论何时,当个 MyWidgetMgr对象需要产生一个 Widget对象时它便调用它的policy了对象 OpNewCreator所提供的 Create()选择“生成策略( Creation policy)是 WidgetManager使用者的权利。藉由这样的设计,可以让 WidgetManager使用者自行装配他所需要的机能。这便是 policy-based class的设计主旨。

运用 Template Template参数实作 Policy Classes

如同先前例子所示, policy的 template引数往往是余的。使用者每每需要传入 template引数给 Opnewcreator,这很笨拙。一般来说, host class已经知道 policy class所需的参数,或是轻易使可推导出来。上述例子中 WidgetManager总是操作 Widget对象,这种情况下还要求使用者“把 Widget型别传给 Opnewcreator”就显得多余而且危险。 这候程序库可以使用“ template template参数”来描述 policies,如下所示:

1
2
3
4
5
// Library code
template <template<classCreated> class CreationPolicy>
class WidgetManager: public Creatlonpollcy<Widget>
//译注: Created是 CreationPolicy的参数, CreationPolicy则是 WidgetManager
//的参数。 Widget已经写入上述程序库中,所以使用时不需要再传一次参数给 policys

尽管露了脸,上述 Created也并未对 WidgetManager有任何贡献。你不能在 WidgetManager中使用 Created,它只是 CreationPolicy(而非 Wi dgetManager)的形式引数( formal argument),此可以省略。 应用端现在只需在使用 WidgetManager时提供 template名称即可:

1
2
// Application code
typedef WidgetManager<OpNewCreator> MyWidgetMgr;

搭配 policy class使用“ template template参数”,并不单纯只为了方便。有时候这种用法不可或缺,以便 host class可藉由 templates产生不同型别的对象。举个例子,假设 WidgetManager想要以相同的生成策略产生一个 Gadget对象,代码如下:

1
2
3
4
5
6
7
8
9
//Library code
template <template class> class CreationPolicy>
class WidgetManager: public CreationPolicy<Widget>
{
void Dosomething ()
{
Gadget* pw=CreationPolicy<Gadget>().Create();
}
}

使用 policies是否会为我们带来一些优势呢?作看之下并不太多。首先, Creator policy的实作木来就十分简短。当然, WidgetManager的作者应该会把“生成对象”的那份代码写成 inline函数,并避开“将 WidgetManager建立为一个 template”时可能遭遇的问题。

然而, policy的确能够带给 WidgetManager非常大的弹性。第一,当你准备具现化( instantiating) WidgetManager时,你可以从外部变更 policies,就和改变 template引数一样简单。第二,你可以根据程序的特殊需求,提供自己的 policies。你可以采用new或malloc或 prototypes或一个专用于你自己系统上的罕见内存分配器。 WidgetManager就像一个小型的“代码生成引擎”( code generation engine),你可以自行设定代码的产生方式。 为了让应用程序开发入员的日子更轻松, WidgetManager的作者应该定义一些常用的 policies,并且以“ template缺省引数”的形式提供最常用的 policy

1
2
template <template <class> class Creation=Opnewcreator> 
class WidgetManager...

注意, policies和虚函数有很大不同。虽然虚函数也提供类似效果: class 作者以基本的( primitive)虚函数来建立高端功能,并允许使用者改写这些基本虚函数的行为。然而如前所示, policies因为有丰富的型别信息及静态连接等特性,所以是建立“设计元素”时的本质性东西。不正是“设计”指定了“执行前型别如何互相作用、你能够做什么、不能够做什么”的完整规则吗? Policies可以让你在型别安全( typesafe)的前提下藉由组合各个简单的需求来产出你的设计。此外,由于编译期才将 host class和其 policies结合在一起,所以和手工打造的程序比较起来更加牢固并且更有效率。 当然,也由于 policies的特质,它们不适用于动态连结和二进位接口,所以本质上 policies和传统接口并不互相争。

运用 Template成员函数实作 Policy Classes

另外一种使用“ template template参数”的情况是把 template成员函数用来连接所需的简单类。也就是说,将 policy实作为一般 class(“一般”是相对于 class template而言),但有一个或数个 templated members. 例如,我们可以重新定义先前的 Creator policy成为一个 non-template class,其内提供一个名为 create的 template函数。如此一来, policy class看起来像下面这个样子.

1
2
3
4
5
6
7
8
struct OpNewCreator
{
template <class T>
static T* Create()
{
return new T;
}
}

这种方式所定义并实作出来的 policy,对于旧式编译器有较佳兼容性。但从另一方面来说,这样的 policy难以讨论、定义、实作和运用。

更丰富的 Policies

Creator policy只指定了一个 Create()成员函数。然而 Prototypecreator却多定义了两个函数,分别为 GetPrototype()和 Setprotorype()。让我们来分析一下。由于 WidgetManager继承了 policy class,而且 GetPrototype()和 Setprototype()是Prototype Creator的 public成员,所以这两个函数使被加至 WidgetManager,并且可以直接被使用者取用。 然而 WidgetManager只要求 Create();那是 WidgetManager切所需,也是用来保证它自己机能的唯一要求。不过使用者可以开发出更丰富的接口。 prototype-based Creator policy class的使用者可以写出下列代码:

1
2
3
4
5
6
typedef WidgetManager<Prototypecreator> MyWidqetManager
...
Widget* prototype = ...;
MyWidqetManager mri;
mgr.Setprototype(prototype);
...use mgr...

如果此后使用者决定采用一个不支持 prototypes的生成策略,那么编译器会指出问题: prototype专属接口已经被用上了。这正是我们希望获得的坚固设计。 如此的结果是很受欢迎的。使用者如果需要扩充 policies,可以在不影响 host class 原本功能的前提下,从更丰富的功能中得到好处。别忘了,决定“哪个 policy被使用”的是使用者而非程序库自身。和般多重接口不同的是, policies给予使用者一种能力,在型别安全( typesafe的前提下扩增 host class的功能.

Policy Classes的析构函数 ( Destructors)

有一个关于建立 policy classes 的要细节。大部分情况下 host class会以 public继承”方式从某些 policies派生而来。因此,使用者可以将一个 host class 自动转为一个 policy class(译注:向上转型),并于稍后 delete该指针。除非 policy class定义了一个虚析构函数( virtual destructor),否则 delete个指向 policy class f的指针,会产生不可预期的结果4,如下所示:

1
2
3
4
typedef WidgetManager<Prototypecreator> MyWidgetManager;
MyWidgetManager wm;
PrototypeCreator<Widget>* pCreator = &wm; // dubious, but legal
delete pCreator; // compiles fine, but has undefined behavior

然而如果为 policy定义了一个虚析构函数,会妨碍 policy的静态连结特性,也会影响执行效率。

许多 policies并无任何数据成员,纯粹只规范行为。第一个虚函数被加入后会为对象大小带来额外开销(译注:因为引入一份vptr),所以虚析构函数应该尽可能避免。 一个解法是,当 host class 自 policy class派生时,采用 protected继承或 private继承。然而这样会失去丰富的 policies特性。 policies应该采用一个轻便而有效率的解法一定义一个non-virtual protected析构函数.

1
2
3
4
5
6
7
8
9
10
struct OpNewCreator
{
template <class T>
static T* Create()
{
return new T;
}
protected:
~Opnewcreator();
}

由于析构函数属于 protected层级,所以只有派生而得的 classes才可以摧毁这个 policy对象。这样一来外界就不可能 delete一个指向 policy class 的指针。而由于析构函数并非虚函数,所以不会有大小或速度上的额外开销。

参考文献

《Modern C++ Design-C++设计新思维》