包裝外觀

包裝外觀,在更為簡潔、健壯、可移植和可維護的較高級面向對象類接口中封裝低級函式和數據結構。

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,他們的大量意見極大地改善了包裝外觀模式描述的形式和內容。

相關詞條

熱門詞條

聯絡我們