包裝外觀(Wrapper Facade):用於在類中封裝函數的結構型模式

包裝外觀(Wrapper Facade):用於在類中封裝函數的結構型模式
 
Douglas C. Schmidt著   馬維達譯

1 介紹
  本論文描述包裝外觀模式。該模式的意圖是通過面向對象(OO)類接口來封裝低級函數和數據結構。常見的包裝外觀模式的例子是像MFC 、ACE和AWT這樣的類庫,它們封裝本地的OS C API,比如socket、pthreads或GUI函數。
  直接對本地OS C API進行編程會使網絡應用繁瑣、不健壯、不可移植,且難以維護,因爲應用開發者需要了解許多低級、易錯的細節。本論文闡釋包裝外觀模式怎樣使這些類型的應用變得更爲簡潔、健壯、可移植和可維護。
  本論文被組織如下:2詳細描述使用西門子格式[1]的包裝外觀模式,3給出結束語。
 
2 包裝外觀模式
 
2.1 意圖
  在更爲簡潔、健壯、可移植和可維護的較高級面向對象類接口中封裝低級函數和數據結構。
 
2.2 例子
  爲闡釋包裝外觀模式,考慮圖2-1中所示的分佈式日誌服務的服務器。客戶應用使用日誌服務來記錄關於它們在分佈式環境中的執行狀態的信息。這些狀態信息通常包括錯誤通知、調試跟蹤和性能診斷。日誌記錄被髮送到中央日誌服務器,由它將記錄寫到各種輸出設備,比如網絡管理控制檯、打印機或數據庫。

 


圖1 分佈式日誌服務

 
  圖1所示的日誌服務器處理客戶發送的連接請求和日誌記錄。日誌記錄和連接請求可以併發地在多個socket句柄上到達。各個句柄標識在 OS中管理的網絡通信資源。
  客戶使用像TCP[2]這樣的面向連接協議與日誌服務器通信。因而,當客戶想要記錄日誌數據時,它必須首先向日誌服務器發送連接請求。服務器使用句柄工廠(handle factory)來接受連接請求,句柄工廠在客戶知道的網絡地址上進行偵聽。當連接請求到達時,OS句柄工廠接受客戶的連接,並創建表示該客戶的連接端點的socket句柄。該句柄被返回給日誌服務器,後者在這個句柄和其他句柄上等待客戶日誌請求到達。一旦客戶被連接,它們可以發送日誌記錄給服務器。服務器通過已連接的socket句柄來接收這些記錄,處理記錄,並將它們寫到它們的輸出設備。
  開發併發處理多個客戶的日誌服務器的常見方法是使用低級C語言函數和數據結構來完成線程、同步及網絡通信等操作。例如,圖2演示怎樣將Solaris線程[3]和socket[4]網絡編程API用於開發多線程日誌服務器。

 


圖2 多線程日誌服務器


 
  在此設計中,日誌服務器的句柄工廠在它的主線程中接受客戶網絡連接。它隨即派生一個新線程,在單獨的連接中運行 logging_handler函數、以處理來自每個客戶的日誌記錄。下面的兩個C函數演示怎樣使用socket、互斥體和線程的本地Solaris OS API來實現此日誌服務器設計。
 
// At file scope.
// Keep track of number of logging requests.
static int request_count;
// Lock to protect request_count.
static mutex_t lock;
// Forward declaration.
static void *logging_handler (void *);
// Port number to listen on for requests.
static const int logging_port = 10000;
 
// Main driver function for the multi-threaded
// logging server. Some error handling has been
// omitted to save space in the example.
int main (int argc, char *argv[])
{
struct sockaddr_in sock_addr;
 
// Handle UNIX/Win32 portability differences.
#if defined (_WINSOCKAPI_)
SOCKET acceptor;
#else
int acceptor;
#endif /* _WINSOCKAPI_ */
 
// Create a local endpoint of communication.
acceptor = socket (PF_INET, SOCK_STREAM, 0);
 
// Set up the address to become a server.
memset (reinterpret_cast <void *> (&sock_addr), 0, sizeof sock_addr);
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons (logging_port);
sock_addr.sin_addr.s_addr = htonl (INADDR_ANY);
 
// Associate address with endpoint.
bind (acceptor,
reinterpret_cast <struct sockaddr *>
(&sock_addr),
sizeof sock_addr);
 
// Make endpoint listen for connections.
listen (acceptor, 5);
 
// Main server event loop.
for (;;)
{
thread_t t_id;
 
// Handle UNIX/Win32 portability differences.
#if defined (_WINSOCKAPI_)
SOCKET h;
#else
int h;
#endif /* _WINSOCKAPI_ */
 
// Block waiting for clients to connect.
int h = accept (acceptor, 0, 0);
 
// Spawn a new thread that runs the
// <logging_handler> entry point and
// processes client logging records on
// socket handle <h>.
thr_create (0, 0,
logging_handler,
reinterpret_cast <void *> (h),
THR_DETACHED,
&t_id);
}
 
/* NOTREACHED */
return 0;
}
 
logging_handler函數運行在單獨的線程控制中,也就是,每個客戶一個線程。它在各個連接上接收並處理日誌記錄,如下所示:
 
// Entry point that processes logging records for
// one client connection.
void *logging_handler (void *arg)
{
// Handle UNIX/Win32 portability differences.
#if defined (_WINSOCKAPI_)
SOCKET h = reinterpret_cast <SOCKET> (arg);
#else
int h = reinterpret_cast <int> (arg);
#endif /* _WINSOCKAPI_ */
for (;;)
{
UINT_32 len; // Ensure a 32-bit quantity.
char log_record[LOG_RECORD_MAX];
 
// The first <recv> reads the length
// (stored as a 32-bit integer) of
// adjacent logging record. This code
// does not handle "short-<recv>s".
ssize_t n = recv
(h,
reinterpret_cast <char *> (&len),
sizeof len, 0);
 
// Bail out if we don’t get the expected len.
if (n <= sizeof len) break;
len = ntohl (len); // Convert byte-ordering.
if (len > LOG_RECORD_MAX) break;
 
// The second <recv> then reads <len>
// bytes to obtain the actual record.
// This code handles "short-<recv>s".
for (size_t nread = 0;
nread < len;
nread += n)
{
n = recv (h, log_record + nread, len - nread, 0);
// Bail out if an error occurs.
if (n <= 0) return 0;
}
 
mutex_lock (&lock);
 
// Execute following two statements in a
// critical section to avoid race conditions
// and scrambled output, respectively.
++request_count; // Count # of requests received.
 
if (write (1, log_record, len) == -1)
// Unexpected error occurred, bail out.
break;
 
mutex_unlock (&lock);
}
 
close (h);
return 0;
}
 
注意全部的線程、同步及網絡代碼是怎樣使用Solaris操作系統所提供的低級C函數和數據結構來編寫的。
 
2.3 上下文
  訪問由低級函數和數據結構所提供服務的應用。
 
2.4 問題
  網絡應用常常使用2.2中所演示的低級函數和數據結構來編寫。儘管這是一種慣用方法,由於不能解決以下問題,它會給應用開發者造成許多問題:
 
繁瑣、不健壯的程序:直接對低級函數和數據結構編程的應用開發者必須反覆地重寫大量冗長乏味的軟件邏輯。一般而言,編寫和維護起來很乏味的代碼常常含有微妙而有害的錯誤。
例如,在2.2的main函數中創建和初始化接受器socket的代碼是容易出錯的,比如沒有對sock_addr清零,或沒有對logging_port號使用htons[5]。mutex_lock和mutex_unlock也容易被誤用。例如,如果write調用返回-1,logging_handler代碼就會不釋放互斥鎖而跳出循環。同樣地,如果嵌套的for循環在遇到錯誤時返回,socket句柄h就不會被關閉。
 
缺乏可移植性:使用低級函數和數據結構編寫的軟件常常不能在不同的OS平臺和編譯器間移植。而且,它們甚至常常不能在同一OS或編譯器的不同版本間移植。不可移植性源於隱藏在基於低級API的函數和數據結構中的信息匱乏。
例如,在2.2中的日誌服務器實現已經硬編碼了對若干不可移植的本地OS線程和網絡編程C API的依賴。特別地,對thr_create、 mutex_lock和mutex_unlock的使用不能移植到非Solaris OS平臺上。同樣地,特定的socket特性,比如使用int表示socket句柄,不能移植到像Win32的WinSock這樣的非Unix平臺上;WinSock將socket句柄表示爲指針。
 
高維護開銷:C和C++開發者通常通過使用#ifdef在他們的應用源碼中顯式地增加條件編譯指令來獲得可移植性。但是,使用條件編譯來處理平臺特有的變種在各方面都增加了應用源碼的物理設計複雜性[6]。開發者難以維護和擴展這樣的軟件,因爲平臺特有的實現細節分散在應用源文件的各個地方。
例如,處理socket數據類型的Win32和UNIX可移植性(也就是,SOCKET vs. int)的#ifdef妨礙了代碼的可讀性。對像這樣的低級C API進行編程的開發者必須十分熟悉許多OS平臺特性,才能編寫和維護該代碼。
由於有這些缺點,通過對低級函數和數據結構直接進行編程來開發應用常常並非是有效的設計選擇。
 
2.5 解決方案
  確保應用不直接訪問低級函數和數據結構的一種有效途徑是使用包裝外觀模式。對於每一組相關的函數和數據結構,創建一或多個包裝外觀類,在包裝外觀接口所提供的更爲簡潔、健壯、可移植和可維護的方法中封裝低級函數和數據結構。
 
2.6 結構
  包裝外觀模式的參與者的結構在下面的UML類圖中演示:

 

包裝外觀模式中的關鍵參與者包括:
 
函數(Function):函數是現有的低級函數和數據結構,它們提供內聚的(cohesive)服務。
 
包裝外觀(Wrapper Fa?ade):包裝外觀是封裝函數和與其相關聯的數據結構的一個或一組類。包裝外觀提供的方法將客戶調用轉發給一或多個低級函數。
 
2.7 動力特性
  下圖演示包裝外觀模式中的各種協作:

 


 
  如下所述,這些協作是十分簡單明瞭的:
 
1. 客戶調用(Client invocation):客戶通過包裝外觀的實例來調用方法。
2. 轉發(Forwarding):包裝外觀方法將請求轉發給它封裝的一或多個底層函數,並傳遞函數所需的任何內部數據結構。
 
2.8 實現
  這一部分解釋通過包裝外觀模式實現組件和應用所涉及的步驟。我們將闡釋這些包裝外觀是怎樣克服繁瑣、不健壯的程序、缺乏可移植性,以及高維護開銷等問題的;這些問題折磨着使用低級函數和數據結構的解決方案。
  這裏介紹的例子基於2.2描述的日誌服務器,圖2-3演示此例中的結構和參與者。這一部分中的例子應用了來自ACE框架[7]的可複用組件。ACE提供一組豐富的可複用C++包裝和框架組件,以跨越廣泛的OS平臺完成常見的通信軟件任務。

 

圖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 <void *> (&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 <sockaddr *>(&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 <stream>.
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);
}
// ...
};
 
Thread_Manager還提供聯接(join)和取消線程的方法。
 
1. 確定錯誤處理機制:低級的C函數API通常使用返回值和整型代碼(比如errno)來將錯誤通知給它們的調用者。但是,此技術是容易出錯的,因爲調用者可能會忘記檢查它們的函數調用的返回狀態。
更爲優雅的報告錯誤的方式是使用異常處理。許多編程語言,比如C++和Java,使用異常處理來作爲錯誤報告機制。它也被某些操作系統所使用,比如Win32。
使用異常處理作爲包裝外觀類的錯誤處理機制有若干好處:
 
· 它是可擴展的:現代編程語言允許通過對現有接口和使用干擾極少的特性來擴展異常處理策略和機制。例如,C++和Java使用繼承來定義異常類的層次。
· 它使錯誤處理與正常處理得以乾淨地去耦合:例如,錯誤處理信息不會顯式地傳遞給操作。而且,應用不會因爲沒有檢查函數返回值而偶然地忽略異常。
· 它可以是類型安全的:在像C++和Java這樣的語言中,異常以一種強類型化的方式被扔出和捕捉,以增強錯誤處理代碼的組織和正確性。相對於顯式地檢查線程專有的錯誤值,編譯器會確保對於每種類型的異常,將執行正確的處理器。
 
但是,爲包裝外觀類使用異常處理也有若干缺點:
 
· 它不是通用的:不是所有語言都提供異常處理。例如,某些C++編譯器沒有實現異常。同樣地,當OS提供異常服務時,它們必須被語言擴展所支持,從而降低了代碼的可移植性。
· 它使多種語言的使用變得複雜化:因爲語言以不同的方式實現異常,或根本不實現異常,如果以不同語言編寫的組件扔出異常,可能很難把它們集成在一起。相反,使用整型值或結構來報告錯誤信息提供了更爲通用的解決方案。
· 它使資源管理變得複雜化:如果在C++或Java代碼塊中有多個退出路徑,資源管理可能會變得複雜化[10]。因而,如果語言或編程環境不支持垃圾收集,必須注意確保在有異常扔出時刪除動態分配的對象。
· 它有着潛在的時間和/或空間低效的可能性:即使沒有異常扔出,異常處理的糟糕實現也會帶來時間和/或空間的過度開銷[10]。對於必須具有高效和低內存佔用特性的嵌入式系統來說,這樣的開銷可能會特別地成問題。
 
  對於封裝內核級設備驅動程序或低級的本地OS API(它們必須被移植到許多平臺上)的包裝外觀來說,異常處理的缺點也是特別成問題的。對於這些類型的包裝外觀,更爲可移植、高效和線程安全的處理錯誤的方式是定義錯誤處理器抽象,顯式地維護關於操作的成功或失敗的信息。使用線程專有存儲(Thread-Specific Storage)模式[11]是被廣泛用於這些系統級包裝外觀的解決方案。
 
1. 定義相關助手類(可選):一旦低級函數和數據結構被封裝在內聚的包裝外觀類中,常常有可能創建其他助手類來進一步簡化應用開發。通常要在包裝外觀模式已被應用於將低級函數和與其關聯的數據聚合進類中之後,這些助手類的效用才變得明顯起來。
例如,在我們的日誌例子中,我們可以有效地利用下面的實現C++ Scoped Locking習語的Guard類;該習語確保Thread_Mutex被適當地釋放,不管程序的控制流是怎樣退出作用域的。
 
template <class LOCK>
class Guard
{
public:
Guard (LOCK &lock): lock_ (lock)
{
lock_.acquire ();
}
 
?Guard (void)
{
lock_.release ();
}
 
private:
// Hold the lock by reference to avoid
// the use of the copy constructor...
LOCK &lock_;
}
 
Guard類應用了[12]中描述的C++習語,藉此,在一定作用域中“構造器獲取資源而析構器釋放它們”。如下所示:
 
// ...
{
// Constructor of <mon> automatically
// acquires the <mutex> lock.
Guard<Thread_Mutex> mon (mutex);
 
// ... operations that must be serialized ...
 
// Destructor of <mon> automatically
// releases the <mutex> lock.
}
// ...
 
  因爲我們使用了像Thread_Mutex包裝外觀這樣的類,我們可以很容易地替換不同類型的鎖定機制,與此同時仍然複用Guard的自動鎖定 /解鎖協議。例如,我們可以用Process_Mutex類來取代Thread_Mutex類,如下所示:
 
// Acquire a process-wide mutex.
Guard<Process_Mutex> mon (mutex);
 
  如果使用C函數和數據結構、而不是C++類,獲得這種程度的“可插性”(pluggability)要困難得多。
 
2.9 例子解答
  下面的代碼演示日誌服務器的main函數,它已使用2.8描述的互斥體、socket和線程的包裝外觀重寫。
 
// At file scope.
// Keep track of number of logging requests.
static int request_count;
 
// Manage threads in this process.
static Thread_Manager thr_mgr;
 
// Lock to protect request_count.
static Thread_Mutex lock;
 
// Forward declaration.
static void *logging_handler (void *);
 
// Port number to listen on for requests.
static const int logging_port = 10000;
 
// Main driver function for the multi-threaded
// logging server. Some error handling has been
// omitted to save space in the example.
int main (int argc, char *argv[])
{
// Internet address of server.
INET_Addr addr (port);
 
// Passive-mode acceptor object.
SOCK_Acceptor server (addr);
 
SOCK_Stream new_stream;
 
// Wait for a connection from a client.
for (;;)
{
// Accept a connection from a client.
server.accept (new_stream);
 
// Get the underlying handle.
SOCKET h = new_stream.get_handle ();
 
// Spawn off a thread-per-connection.
thr_mgr.spawn (logging_handler,
reinterpret_cast <void *> (h),
THR_DETACHED);
}
}
 
logging_handler函數運行在單獨的線程控制中,也就是,每個相連客戶有一個線程。它在各個連接上接收並處理日誌記錄,如下所示:
 
// Entry point that processes logging records for
// one client connection.
void *logging_handler (void *arg)
{
SOCKET h = reinterpret_cast <SOCKET> (arg);
 
// Create a <SOCK_Stream> object from SOCKET <h>.
SOCK_Stream stream (h);
for (;;)
{
UINT_32 len; // Ensure a 32-bit quantity.
char log_record[LOG_RECORD_MAX];
 
// The first <recv_n> reads the length
// (stored as a 32-bit integer) of
// adjacent logging record. This code
// handles "short-<recv>s".
ssize_t n = stream.recv_n
(reinterpret_cast <char *> (&len),
sizeof len);
 
// Bail out if we’re shutdown or
// errors occur unexpectedly.
if (n <= 0) break;
len = ntohl (len); // Convert byte-ordering.
if (len > LOG_RECORD_MAX) break;
 
// The second <recv_n> then reads <len>
// bytes to obtain the actual record.
// This code handles "short-<recv>s".
n = stream.recv_n (log_record, len);
 
// Bail out if we’re shutdown or
// errors occur unexpectedly.
if (n <= 0) break;
 
{
// Constructor of Guard automatically
// acquires the lock.
Guard<Thread_Mutex> mon (lock);
 
// Execute following two statements in a
// critical section to avoid race conditions
// and scrambled output, respectively.
++request_count; // Count # of requests
 
if (write (STDOUT, log_record, len) == -1)
break;
 
// Destructor of Guard automatically
// releases the lock, regardless of
// how we exit this block!
}
}
 
// Destructor of <stream> automatically
// closes down <h>.
return 0;
}
 
注意上面的代碼是怎樣解決2.2所示代碼的各種問題的。例如,SOCK_Stream和Guard的析構器會分別關閉socket句柄和釋放 Thread_Mutex,而不管代碼塊是怎樣退出的。同樣地,此代碼要容易移植和維護得多,因爲它沒有使用平臺特有的API。


2.10 已知應用
  本論文中的例子聚焦於併發網絡編程。但是,包裝外觀模式已被應用到其他的許多領域,比如GUI框架和數據庫類庫。下面是包裝外觀模式的一些廣爲人知的應用:
 
Microsoft Foundation Class(MFC):MFC提供一組封裝大多數低級C Win32 API的包裝外觀,主要集中於提供實現Microsoft文檔/模板體系結構的GUI組件。
 
ACE框架:2.8描述的互斥體、線程和socket的包裝外觀分別基於ACE框架中的組件[7]:ACE_Thread_Mutex、ACE_Thread_Manager 和ACE_SOCK*類。
 
Rogue Wave類庫:Rogue Wave的Net.h++和Threads.h++類庫在許多OS平臺上實現了socket、線程和同步機制的包裝外觀。
 
ObjectSpace System<Toolkit>:該工具包也提供了socket、線程和同步機制的包裝外觀。
 
Java虛擬機和Java基礎類庫:Java虛擬機(JVM)和各種Java基礎類庫,比如AWT和Swing,提供了一組封裝大多數低級的本地OS系統調用和GUI API的包裝外觀。
 
2.11 效果
  包裝外觀模式提供以下好處:
 
更爲簡潔和健壯的編程接口:包裝外觀模式在一組更爲簡潔的OO類方法中封裝許多低級函數。這減少了使用低級函數和數據結構開發應用的枯燥性,從而降低了發生編程錯誤的潛在可能性。
 
改善應用可移植性和可維護性:包裝外觀類的實現可用以使應用開發者與低級函數和數據結構的不可移植的方面屏蔽開來。而且,通過用基於邏輯設計實體(比如基類、子類,以及它們的關係)的應用配置策略取代基於物理設計實體(比如文件和#ifdef)的策略[6],包裝外觀模式改善了軟件結構。一般而言,根據應用的邏輯設計、而不是物理設計來理解和維護它們要更爲容易一些。
 
改善應用的模塊性、可複用性和可配置性:通過使用像繼承和參數化類型這樣的OO語言特性,包裝外觀模式創建的可複用類組件可以一種整體方式被“插入”其他組件,或從中“拔出”。相反,不求助於粗粒度的OS工具,比如鏈接器或文件系統,替換成組的函數要難得多。
 
包裝外觀模式有以下缺點:
 
額外的間接性(Indirection):與直接使用低級的函數和數據結構相比,包裝外觀模式可能帶來額外的間接。但是,支持內聯的語言,比如C++,可以無需顯著的開銷而實現該模式,因爲編譯器可以內聯用於實現包裝外觀的方法調用。


2.12 參見
  包裝外觀模式與外觀模式是類似的[8]。外觀模式的意圖是簡化子系統的接口。包裝外觀模式的意圖則更爲具體:它提供簡潔、健壯、可移植和可維護的類接口,封裝低級的函數和數據結構,比如本地OS互斥體、socket、線程和GUI C語言API。一般而言,外觀將複雜的類關係隱藏在更簡單的API後面,而包裝外觀將複雜的函數和數據結構關係隱藏在更豐富的類API後面。
  如果動態分派被用於實現包裝外觀方法,包裝外觀模式可使用橋接模式[8]來實現;包裝外觀方法在橋接模式中扮演抽象( Abstraction)角色。
 
3 結束語
  本論文描述包裝外觀模式,並給出了詳細的例子演示怎樣使用它。在本論文中描述的ACE包裝外觀組件的實現可在ACE[7]軟件發佈中自由獲取(URL:http://www.cs.wustl.edu/~schmidt/ACE.html)。該發佈含有在聖路易斯華盛頓大學開發的完整的C++源碼、文檔和測試例子驅動程序。目前ACE正在用於許多公司(像Bellcore、波音、DEC、愛立信、柯達、朗訊、摩托羅拉、SAIC和西門子)的通信軟件項目中。
感謝
  感謝Hans Rohnert、Regine Meunier、Michael Stal、Christa Schwanninger、Frank Buschmann和Brad Appleton,他們的大量意見極大地改善了包裝外觀模式描述的形式和內容。
 
參考文獻
[1] F. Buschmann, R. Meunier, H. Rohnert, P. Sommerlad, and M. Stal, Pattern-Oriented Software Architecture - A System of Patterns. Wiley and Sons, 1996.
[2] W.R.Stevens,UNIX Network Programming, First Edition. Englewood Cliffs, NJ: Prentice Hall, 1990.
[3] J. Eykholt, S. Kleiman, S. Barton, R. Faulkner, A. Shivalin-giah, M. Smith, D. Stein, J. Voll, M. Weeks, and D. Williams, “Beyond Multiprocessing... Multithreading the SunOS Ker-nel,” in Proceedings of the Summer USENIX Conference,(San Antonio, Texas), June 1992.
[4] W.R.Stevens,UNIX Network Programming, Second Edition. Englewood Cliffs, NJ: Prentice Hall, 1997.
[5] D. C. Schmidt, “IPC SAP: An Object-Oriented Interface to Interprocess Communication Services,” C++ Report,vol.4, November/December 1992.
[6] J. Lakos, Large-scale Software Development with C++. Reading, MA: Addison-Wesley, 1995.
[7] D. C. Schmidt, “ACE: an Object-Oriented Framework for Developing Distributed Applications,” in Proceedings of the 6th USENIX C++ Technical Conference, (Cambridge, Mas-sachusetts), USENIX Association, April 1994.
[8] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Pat-terns: Elements of Reusable Object-Oriented Software. Read-ing, MA: Addison-Wesley, 1995.
[9] D. C. Schmidt, “Acceptor and Connector: Design Patterns for Initializing Communication Services, ” in Pattern Languages of Program Design (R. Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997.
[10] H. Mueller, “Patterns for Handling Exception Handling Suc-cessfully,” C++ Report, vol. 8, Jan. 1996.
[11] D. C. Schmidt, T. Harrison, and N. Pryce, “Thread-Specific Storage – An Object Behavioral Pattern for Accessing per-Thread State Efficiently,” C++ Report,vol.9,Novem-ber/December 1997.
[12] Bjarne Stroustrup, The C++ Programming Language, 3rd Edition. Addison-Wesley, 1998.

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/bolidecaster/archive/2003/01/07/19056.aspx

發佈了3 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章