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
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 | template <class T> class Widget |
但是,下面是我被告知的错误。
1 | template<class T, class U> class Gadget |
仅对一个成员函数进行部分特化是不可能的,而必须对整个类进行部分特化。这就是 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 | template <class T> |
任何一个 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 | //Library code |
如果 class 采用一个或多个 policies,我们称其为 hosts 或 host classes。上例的 WidgetManager 使是“采用了个 policy'”的 host class. Hosts 负责把 policies 提供的结构和行为组成一个更复杂的结构和行为。 当客户端将 WidgetManager template 具现化( Instantiating)时,必须传进一个他所期望的 policy:
1 | // Application code |
让我们分析整个来龙去脉。无论何时,当个 MyWidgetMgr 对象需要产生一个 Widget 对象时它便调用它的 policy 了对象 OpNewCreator
运用 Template Template 参数实作 Policy Classes
如同先前例子所示, policy 的 template 引数往往是余的。使用者每每需要传入 template 引数给 Opnewcreator,这很笨拙。一般来说, host class 已经知道 policy class 所需的参数,或是轻易使可推导出来。上述例子中 WidgetManager 总是操作 Widget 对象,这种情况下还要求使用者“把 Widget 型别传给 Opnewcreator”就显得多余而且危险。 这候程序库可以使用“ template template 参数”来描述 policies,如下所示:
1 | // Library code |
尽管露了脸,上述 Created 也并未对 WidgetManager 有任何贡献。你不能在 WidgetManager 中使用 Created,它只是 CreationPolicy(而非 Wi dgetManager)的形式引数( formal argument),此可以省略。 应用端现在只需在使用 WidgetManager 时提供 template 名称即可:
1 | // Application code |
搭配 policy class 使用“ template template 参数”,并不单纯只为了方便。有时候这种用法不可或缺,以便 host class 可藉由 templates 产生不同型别的对象。举个例子,假设 WidgetManager 想要以相同的生成策略产生一个 Gadget 对象,代码如下:
1 | //Library code |
使用 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 | template <template <class> class Creation=Opnewcreator> |
注意, 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
1 | struct OpNewCreator |
这种方式所定义并实作出来的 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 | typedef WidgetManager<Prototypecreator> MyWidqetManager |
如果此后使用者决定采用一个不支持 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 | typedef WidgetManager<Prototypecreator> MyWidgetManager; |
然而如果为 policy 定义了一个虚析构函数,会妨碍 policy 的静态连结特性,也会影响执行效率。
许多 policies 并无任何数据成员,纯粹只规范行为。第一个虚函数被加入后会为对象大小带来额外开销(译注:因为引入一份 vptr),所以虚析构函数应该尽可能避免。 一个解法是,当 host class 自 policy class 派生时,采用 protected 继承或 private 继承。然而这样会失去丰富的 policies 特性。 policies 应该采用一个轻便而有效率的解法一定义一个 non-virtual protected 析构函数。
1 | struct OpNewCreator |
由于析构函数属于 protected 层级,所以只有派生而得的 classes 才可以摧毁这个 policy 对象。这样一来外界就不可能 delete 一个指向 policy class 的指针。而由于析构函数并非虚函数,所以不会有大小或速度上的额外开销。
参考文献
《Modern C++ Design-C++设计新思维》