ACE技术论文集-第2章 包装外观 Wrapper Facade
发布: 2008-6-13 15:00 | 作者: Prashant Jain and D | 来源: 转载 | 查看: 423次
如下所述,这些协作是十分简单明了的:
1. 客户调用(Client invocation):客户通过包装外观的实例来调用方法。
2. 转发(Forwarding):包装外观方法将请求转发给它封装的一或多个底层函数,并传递函数所需的任何内部数据结构。
这一部分解释通过包装外观模式实现组件和应用所涉及的步骤。我们将阐释这些包装外观是怎样克服繁琐、不健壮的程序、缺乏可移植性,以及高维护开销等问题的;这些问题折磨着使用低级函数和数据结构的解决方案。
这里介绍的例子基于2.2.2描述的日志服务器,图2-3演示此例中的结构和参与者。这一部分中的例子应用了来自ACE构架[7]的可复用组件。ACE提供一组丰富的可复用C++包装和构架组件,以跨越广泛的OS平台完成常见的通信软件任务。

图2-3 多线程日志服务器
可采取下面的步骤来实现包装外观模式:
1. 确定现有函数间的内聚的抽象和关系:像Win32、POSIX或X Windows这样被实现为独立的函数和数据结构的传统API提供许多内聚的抽象,比如用于网络编程、同步和线程,以及GUI管理的机制。但是,由于在像C这样的低级语言中缺乏数据抽象支持,开发者常常并不能马上明了这些现有的函数和数据结构是怎样互相关联的。因此,应用包装外观的第一步就是确定现有API中的较低级函数之间的内聚的抽象和关系。换句话说,我们通过将现有的低级API函数和数据结构聚合进一或多个类中来定义一种“对象模型”。
在我们的日志例子中,我们从仔细检查我们原来的日志服务器实现开始。该实现使用了许多低级函数,由它们实际提供若干内聚的服务,比如同步和网络通信。例如,mutex_lock和mutex_unlock函数与互斥体同步抽象相关联。同样地,socket、bind、listen和accept函数扮演了网络编程抽象的多种角色。
2. 将内聚的函数组聚合进包装外观类和方法中:该步骤可划分为以下子步骤:
在此步骤中,我们为每组相关于特定抽象的函数和数据结构定义一或多个包装外观类。
A. 创建内聚的类:我们从为每组相关于特定抽象的函数和数据结构定义一或多个包装外观类开始。用于创建内聚的类的若干常用标准包括:
- 将具有高内聚性(cohesion)的函数合并进独立的类中,同时使类之间不必要的耦合最小化。
- 确定在底层函数中什么是通用的什么是可变的,并把函数分组进类中,从而将变化隔离在统一的接口后面。
一般而言,如果原来的API含有广泛的相关函数,就有可能必须创建若干包装外观类来适当地对事务进行分理。
B. 将多个独立函数合并进类方法中:除了将现有函数分组进类中,在每个包装类中将多个独立函数组合进数目更少的方法中常常也是有益的。例如,为确保一组低级函数以适当的顺序被调用,可能必须要采用此设计。
C. 选择间接层次:大多数包装外观类简单地将它们的方法调用直接转发给底层的低级函数。如果包装外观方法是内联的,与直接调用低级函数相比,可能并没有额外的间接层次。为增强可扩展性,还可以通过动态分派包装外观方法实现来增加另外的间接层次。在这种情况下,包装外观类扮演桥接(Bridge)模式[8]中的抽象(Abstraction)角色。
D. 确定在哪里处理平台特有的变种:使平台特有的应用代码最少化是使用包装外观模式的重要好处。因而,尽管包装外观类方法的实现在不同的OS平台上可以不同,它们应该提供统一的、平台无关的接口。
处理平台特有变种的一种策略是在包装外观类方法实现中使用#ifdef。在联合使用#ifdef和自动配置工具(比如GNU autoconf)时,可以通过单一的源码树创建统一的、不依赖于平台的包装外观。另一种可选策略是将不同的包装外观类实现分解进分离的目录中(例如,每个平台有一个目录),并配置语言处理工具,以在编译时将适当的包装外观类包含进应用中。
选择特定的策略在很大程度上取决于包装外观方法实现变动的频度。例如,如果它们频繁变动,为每个平台正确地更新#ifdef可能是单调乏味的。同样地,所有依赖于该文件的文件可能都需要重编译,即使变动仅仅对一个平台来说是必需的。
在我们的日志例子中,我们将为互斥体、socket和线程定义包装外观类,以演示每一子步骤是怎样被实施的。如下所示:
- 互斥体包装外观:我们首先定义Thread_Mutex抽象,在统一和可移植的类接口中封装Solaris互斥体函数:
class Thread_Mutex
{
public:
Thread_Mutex (void)
{
mutex_init (&mutex_, 0, 0);
}
?Thread_Mutex (void)
{
mutex_destroy (&mutex_);
}
int acquire (void)
{
return mutex_lock (&mutex_);
}
int release (void)
{
return mutex_unlock (&mutex_);
}
private:
// Solaris-specific Mutex mechanism.
mutex_t mutex_;
// = Disallow copying and assignment.
Thread_Mutex (const Thread_Mutex &);
void operator= (const Thread_Mutex &);
};
通过定义Thread_Mutex类接口,并随之编写使用它、而不是低级本地OS C API的应用,我们可以很容易地将我们的包装外观移植到其他平台。例如,下面的Thread_Mutex实现在Win32上工作:
class Thread_Mutex
{
public:
Thread_Mutex (void)
{
InitializeCriticalSection (&mutex_);
}
?Thread_Mutex (void)
{
DeleteCriticalSection (&mutex_);
}
int acquire (void)
{
EnterCriticalSection (&mutex_); return 0;
}
int release (void)
{
LeaveCriticalSection (&mutex_); return 0;
}
private:
// Win32-specific Mutex mechanism.
CRITICAL_SECTION mutex_;
// = Disallow copying and assignment.
Thread_Mutex (const Thread_Mutex &);
void operator= (const Thread_Mutex &);
};
如早先所描述的,我们可以通过在Thread_Mutex方法实现中使用#ifdef以及自动配置工具(比如GUN autoconf)来支持多个OS平台,以使用单一源码树提供统一的、平台无关的互斥体抽象。相反,我们也可以将不同的Thread_Mutex实现分解进分离的目录中,并指示我们的语言处理工具在编译时将适当的版本包含进我们的应用中。
除了改善可移植性,我们的Thread_Mutex包装外观还提供比直接编程低级Solaris函数和mutex_t数据结构更不容易出错的互斥体接口。例如,我们可以使用C++ private访问控制指示符来禁止互斥体的拷贝和赋值;这样的使用是错误的,但却不会被不那么强类型化的C编程API所阻止。
- socket包装外观:socket API比Solaris互斥体API要大得多,也有表现力得多[5]。因此,我们必须定义一组相关的包装外观类来封装socket。我们将从定义下面的处理UNIX/Win32可移植性差异的typedef开始:
#if !defined (_WINSOCKAPI_)
typedef int SOCKET;
#define INVALID_HANDLE_VALUE -1
#endif /* _WINSOCKAPI_ */
接下来,我们将定义INET_Addr类,封装Internet域地址结构:
class INET_Addr
{
public:
INET_Addr (u_short port, long addr)
{
// Set up the address to become a server.
memset (reinterpret_cast (&addr_), 0, sizeof addr_);
addr_.sin_family = AF_INET;
addr_.sin_port = htons (port);
addr_.sin_addr.s_addr = htonl (addr);
}
u_short get_port (void) const
{
return addr_.sin_port;
}
long get_ip_addr (void) const
{
return addr_.sin_addr.s_addr;
}
sockaddr *addr (void) const
{
return reinterpret_cast (&addr_);
}
size_t size (void) const
{
return sizeof (addr_);
}
// ...
private:
sockaddr_in addr_;
};
注意INET_Addr构造器是怎样通过将sockaddr_in域清零,并确保端口和IP地址被转换为网络字节序,消除若干常见的socket编程错误的。
下一个包装外观类,SOCK_Stream,对应用可在已连接socket句柄上调用的I/O操作(比如recv和send)进行封装:
class SOCK_Stream
{
public:
// = Constructors.
// Default constructor.
SOCK_Stream (void)
: handle_ (INVALID_HANDLE_VALUE) {}
// Initialize from an existing HANDLE.
SOCK_Stream (SOCKET h): handle_ (h) {}
// Automatically close the handle on destruction.
?SOCK_Stream (void) { close (handle_); }
void set_handle (SOCKET h) { handle_ = h; }
SOCKET get_handle (void) const { return handle_; }
// = I/O operations.
int recv (char *buf, size_t len, int flags = 0);
int send (const char *buf, size_t len, int flags = 0);
// ...
private:
// Handle for exchanging socket data.
SOCKET handle_;
};
注意此类是怎样确保socket句柄在SOCK_Stream对象出作用域时被自动关闭的。
SOCK_Stream对象由连接工厂SOCK_Acceptor创建,后者封装被动的连接建立逻辑[9]。SOCK_Acceptor构造器初始化被动模式接受器socket,以在sock_addr地址上进行侦听。同样地,accept工厂方法通过新接受的连接来初始化SOCK_Stream,如下所示:
class SOCK_Acceptor
{
public:
SOCK_Acceptor (const INET_Addr &sock_addr)
{
// Create a local endpoint of communication.
handle_ = socket (PF_INET, SOCK_STREAM, 0);
// Associate address with endpoint.
bind (handle_, sock_addr.addr (), sock_addr.size ());
// Make endpoint listen for connections.
listen (handle_, 5);
};
// Accept a connection and initialize
// the .
int accept (SOCK_Stream &stream)
{
stream.set_handle (accept (handle_, 0, 0));
if (stream.get_handle () == INVALID_HANDLE_VALUE)
return -1;
else return 0;
}
private:
// Socket handle factory.
SOCKET handle_;
};
注意SOCK_Acceptor的构造器是怎样确保低级的socket、bind和listen函数总是以正确的次序被调用的。
完整的socket包装外观集还包括SOCK_Connector,封装主动的连接建立逻辑[9]。
- 线程外观:在不同的OS平台上有许多线程API可用,包括Solaris线程、POSIX Pthreads和Win32线程。这些API显示出微妙的语法和语义差异,例如,Solaris和POSIX线程可以“分离”(detached)模式被派生,而Win32线程则不行。但是,可以提供Thread_Manager包装外观,在统一的API中封装这些差异。如下所示:
class Thread_Manager
{
public:
int spawn (void *(*entry_point) (void *),
void *arg,
long flags,
long stack_size = 0,
void *stack_pointer = 0,
thread_t *t_id = 0)
{
thread_t t;
if (t_id == 0)
t_id = &t;
return thr_create (stack_size,
stack_pointer,
entry_point,
arg,
flags,
t_id);
}
// ...
};
