Poco类继承体系结构特性分析

最近在学习接口类的几种设计方式时,想起了Poco库,看看Poco C++库源码是如何实现接口类。这里所说的接口类在一般概念上讲我会认为是只具有纯虚接口函数声明的类,但是看完Poco的一部分源码后,我觉得“接口类”的概念不仅局限于此,看来我学识尚浅。所幸RP比较给力,让我正好在看《C++代码设计与重用》一书中看到了这两种手法。

继承基类的接口

先说说一般的接口类,通常情况下它是个虚基类,只定义了虚函数接口,这样的优点很多:

  • 向使用者隐藏了接口实现的具体细节

  • 使用者无需知晓接口各个子类的继承体系

  • 最大的优点应该是其语言多态性跨越了二进制统一接口的障碍

我以比较普遍的跨平台接口类设计为例,具体代码可能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class InterfaceBase
{
public:
virtual void InterfaceMethod()=0;
};

class InterfaceImpl : public InterfaceBase
{
public:
virtual void InterfaceMethod()
{// Win32 Implementation
...
}
};

UML类图如下:

UML_1

继承基类的实现(反向接口实现继承)

现在说说Poco C++库中部分类的继承体系结构。

其中许多类采用了一种相反的接口类继承体系,相比于书中的“继承基类的实现”,我更乐意称之为反向接口实现继承结构。也就是说前面所描述的接口类中,各种可能的子类实现方法被放到了继承体系的上层,即作为基类。以Poco的例子来说就是各个平台相关的代码不是继承于某个描述接口的虚基类,而是“接口”类继承

Poco的线程类Thread.h如下,为了便于理解,我去掉了一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Thread.h

#if defined(POCO_OS_FAMILY_WINDOWS)
#if defined(_WIN32_WCE)
#include "Poco/Thread_WINCE.h"
#else
#include "Poco/Thread_WIN32.h"
#endif
#elif defined(POCO_VXWORKS)
#include "Poco/Thread_VX.h"
#else
#include "Poco/Thread_POSIX.h"
#endif

class Thread: private ThreadImpl
{

};

从这段代码中应该看到两点:

  1. ThreadImpl在不同平台的头文件中被声明,并由宏定义控制在编译前就区分各平台。
  2. 关于继承一个类的实现,而不改变其自身实现,最好的方法就是private继承方式,表明子类完全利用基类的函数实现而不暴露给对象外部。(其实这里使用继承的方法还有些内容,我留在文章最后说说)
    因此,所有的实现代码在基类中被区分,不同的平台操作在子类看来都是统一的接口函数,这些函数已经由基类声明并定义了,子类继承而来。

来看Poco库的Thread.cpp中子类是如何调用基类的各个实现函数:

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
// Thread.cpp

void Thread::start(Runnable& target)
{
startImpl(target);
}

void Thread::start(Callable target, void* pData)
{
startImpl(target, pData);
}

void Thread::join()
{
joinImpl();
}

void Thread::join(long milliseconds)
{
if (!joinImpl(milliseconds))
throw TimeoutException();
}

bool Thread::tryJoin(long milliseconds)
{
return joinImpl(milliseconds);
}

所看到的***Impl函数就是在基类ThreadImpl中已经被实现了的函数,子类无需关心各个平台的核心代码如何实现,只需要知道要做出哪些统一简单的调用动作。就好比发射卫星,每个国家都有不同运载方式将卫星送入轨道,但是在外界看来他们都只需要发出一个“发射”命令即可。

UML图如下:

UML_2

个人总结下这两种方式的优缺点:

  1. 首先,无疑前者最大的优势是跨越了ABI(应用程序二进制接口)界限,而后者往往在基类的中包含了各种平台相关的数据结构,无法被各种编译器所相容。
  2. 其次,前者在代码分发上同样具有优势,它只需发布一个简单的接口类声明的头文件即可向外部告之自身的功能。而后者由于连同平台实现的具体类声明头文件也被include了,那无疑会增加各个组件之间的复杂度,并且暴露了平台相关的实现细节。
  3. 但是,Poco是开源的,提倡使用者把Poco代码嵌入到他们的代码中。如果需要分发组件接口,大可以使用包括第一种方法在内的许多种方式。个人认为第二种做法一个很大的优势也就在于:类在模块(或组件)的内部接口共享。这种方法不提倡在模块(或组件)间滥用。个人觉得需要这还是需要程序员自身的觉悟来控制代码复杂度,这应该Poco的理念是一致的:遵守代码之间的约定。
    反过来想想,的确也是如此,很多时候很多代码处我仅仅只需要的是一句简单的:
1
Thread myThread;

而第一种继承基类的接口方法常做的事情就是:

1
2
3
4
ISomeInterface* pInterface;
switch(***)
case ****:
pInterface= new ISomeInterfaceImpl;

好了,再来说说第二种方式继承接口的实现中的继承

好的继承我们总是可以把它用is-a的语义来解释清楚,但是这里的Thread并不是ThreadImpl的一种,因为在编译时期,Thread和ThreadImpl是两个平行的概念。所以有些源码中(包括Poco自身)会以组合的方式出现,比如Poco Net库中的NetworkInterface.h:

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
// NetworkInterface.h
class NetworkInterfaceImpl; //前置声明

class NetworkInterface
{

private:
NetworkInterfaceImpl* _pImpl; // 组合代替继承
};</pre>
NetworkInterface.cpp中同样可以自由的调用接口。
<pre class="brush:cpp">// NetworkInterface.cpp

int NetworkInterface::index() const
{
return _pImpl-&gt;index();
}

const std::string&amp; NetworkInterface::name() const
{
return _pImpl-&gt;name();
}

const std::string&amp; NetworkInterface::displayName() const
{
return _pImpl-&gt;displayName();
}

UML图就不画了,夜深了……