Wrapper Facade 1 Wrapper Facade
Total Page:16
File Type:pdf, Size:1020Kb
Wrapper Facade 1 Wrapper Facade The Wrapper Facade design pattern encapsulates low-level functions and data structures within more concise, robust, portable, and maintainable object-oriented class interfaces. Example Consider you are developing a server for a distributed logging service that can handle multiple clients concurrently using a connection-ori- ented protocol like TCP [Ste90]. When a client wants to log data, it must first send a connection request. The logging server accepts con- nection requests using an acceptor factory, which listens on a net- work address known to clients. When a connection request arrives from a client, the acceptor factory accepts the client's connection and creates a socket handle that rep- resents this client's connection endpoint. This handle is passed up to the logging server, which spawns a thread and waits in this thread for logging requests to arrive on the connected socket handle. Once a cli- ent is connected, it can send logging requests to the server. The server receives these requests via the connected socket handles, processes the logging requests, and writes the requests to a log file. Server Server 1: socket() 2: bind() 3: listen() 8: recv() 5: accept() 9: write() 6: thr_create() 4: connect() Client : Acceptor : Logging 7: send() Factory Handler A common way to develop the logging server is to use low-level C lan- guage functions and data structures for threading, synchronization and network communication, for example by using Solaris threads [EKBF+92] and the socket [Ste97] network programming API. However, if the logging server is expected to run on multiple plat- forms, such as Solaris and Win32, the data structures you need may differ in type, and the functions may differ in their syntax and seman- © Douglas C. Schmidt 1998, 1999, all rights reserved, © Siemens AG 1998, 1999, all rights reserved 23.04.1999 Wrapper-Facade.pub.doc 2 tics. As a result, the implementation will contain code that handles the differences in the Solaris and Win32 operating system APIs for sockets, mutexes, and threads. For example, if written with C APIs, the logging server implementation and the logging handler function, which runs in its own thread, will likely contain many #ifdefs: #if defined (_WIN32) #if defined (_WIN32) #include <windows.h> EnterCriticalSection (&lock); typedef int ssize_t; #else #else mutex_lock (&lock); // The following typedef is platform-specific. #endif /* _WIN32 */ typedef unsigned int UINT32; // Execute following two statements #include <thread.h> // in a critical section to avoid #include <unistd.h> // race conditions and scrambled #include <sys/socket.h> // output, respectively. #include <netinet/in.h> // Count # of requests #include <memory.h> ++request_count; #endif /* _WIN32 */ if (write_record (log_record, len) == -1) break; // Keep track of number of logging requests. static int request_count; #if defined (_WIN32) LeaveCriticalSection (&lock); // Lock to protect request_count. #else #if defined (_WIN32) mutex_unlock (&lock); static CRITICAL_SECTION lock; #endif /* _WIN32 */ #else } static mutex_t lock; #endif /* _WIN32 */ #if defined (_WIN32) closesocket (h); // Maximum size of a logging record. #else static const int LOG_RECORD_MAX = 1024; close (h); #endif /* _WIN32 */ // Port number to listen on for requests. return 0; static const int logging_port = 10000; } // Entry point that writes logging records. // Main driver function for the server. int write_record (char log_record[], int len) { int main (int argc, char *argv[]) { /* ... */ struct sockaddr_in sock_addr; return 0; } // Handle UNIX/Win32 portability. #if defined (_WIN32) // Entry point that processes logging records for SOCKET acceptor; // one client connection. #else #if defined (_WIN32) int acceptor; u_long #endif /* _WIN32 */ #else // Create a local endpoint of communication. void * acceptor = socket (PF_INET, SOCK_STREAM, 0); #endif /* _WIN32 */ // Set up the address to become a server. logging_handler (void *arg) { memset (reinterpret_cast<void *> (&sock_addr), // Handle UNIX/Win32 portability. 0, sizeof sock_addr); #if defined (_WIN32) sock_addr.sin_family = AF_INET; SOCKET h = reinterpret_cast <SOCKET> (arg); sock_addr.sin_port = htons (logging_port); #else sock_addr.sin_addr.s_addr = htonl (INADDR_ANY); int h = reinterpret_cast <int> (arg); // Associate address with endpoint. #endif /* _WIN32 */ bind (acceptor, reinterpret_cast<struct sockaddr *> (&sock_addr), sizeof sock_addr); for (;;) { // Make endpoint listen for connections. #if defined (_WIN32) listen (acceptor, 5); ULONG len; #else // Main server event loop. UINT32 len; for (;;) { #endif /* _WIN32 */ // Handle UNIX/Win32 portability. #if defined (_WIN32) // Ensure a 32-bit quantity. SOCKET h; char log_record[LOG_RECORD_MAX]; DWORD t_id; #else // The first <recv> reads the length int h; // (stored as a 32-bit integer) of thread_t t_id; // adjacent logging record. This code #endif /* _WIN32 */ // does not handle "short-<recv>s". // Block waiting for clients to connect. ssize_t n = recv (h, h = accept (acceptor, 0, 0); reinterpret_cast <char *> (&len), // Spawn a new thread that runs the <server> sizeof len, 0); // entry point. // Bail out if we're shutdown or #if defined (_WIN32) // errors occur unexpectedly. CreateThread (0, 0, if (n <= sizeof len) break; LPTHREAD_START_ROUTINE(&logging_handler), len = ntohl (len); reinterpret_cast <void *> (h), 0, &t_id); if (len > LOG_RECORD_MAX) break; #else thr_create // The second <recv> then reads <len> (0, 0, logging_handler, // bytes to obtain the actual record. reinterpret_cast <void *> (h), // This code handles "short-<recv>s". THR_DETACHED, &t_id); for (ssize_t nread = 0; nread < len; nread += n) { #endif /* _WIN32 */ n = recv (h, log_record + nread, } len - nread, 0); return 0; // Bail out if an error occurs. } if (n <= 0) return 0; } © Douglas C. Schmidt 1998, 1999, all rights reserved, © Siemens AG 1998, 1999, all rights reserved 23.04.1999 Wrapper-Facade.pub.doc Wrapper Facade 3 Even moving platform-specific declarations into separate configura- tion header files does not resolve the problems. For example, #ifdefs that separate the use of platform-specific APIs, such as the thread creation calls, still pollute application code. Likewise, #ifdefs may also be required to work around compiler bugs or lack of features in certain compilers. The problems with different semantics of these APIs also remain. Moreover, adding a new platform still requires ap- plication developers to modify and update the platform-specific dec- larations, whether they are included directly into application code or separated into configuration files. Context Applications that access services provided by low-level functions and data structures. Problem Applications are often written using low-level operating system func- tions and data structures, such as for networking and threading, or other libraries, such as for user interface or database programming. Although this is common practice, it causes problems for application developers by failing to resolve the following problems: • Verbose, non-robust programs. Application developers who program directly to low-level functions and data structures must repeatedly rewrite a great deal of tedious software logic. In general, code that is tedious to write and maintain often contains subtle and perni- cious errors. ➥ The code for creating and initializing an acceptor socket in the main() function of our logging server example is prone to errors. Common errors include failing to zero-out the sock_addr or not using htons on the logging_port number [Sch92]. In particular, note how the lock will not be released if the write_record() function returns -1. ❏ • Lack of portability. Software written using low-level functions and data structures is often non-portable between different operating systems and compilers. Moreover, it is often not even portable to program to low-level functions across different versions of the same operating system or compiler due to the lack of release-to-release compatibility [Box97]. ➥ Our logging server implementation has hard-coded dependen- cies on several non-portable native operating system threading and © Douglas C. Schmidt 1998, 1999, all rights reserved, © Siemens AG 1998, 1999, all rights reserved 23.04.1999 Wrapper-Facade.pub.doc 4 network programming C APIs. For instance, thr_create(), mutex_lock() and mutex_unlock() are not portable to Win32 platforms. Although the code is designed to be quasi-portable—it also compiles and runs on Win32 platforms—there are various subtle portability problems. In particular, there will be resource leaks on Win32 platforms since there is no equivalent to the Solaris THR_DETACHED feature, which spawns a ‘detached’ thread whose exit status is not stored by the threading library. ❏ • High maintenance effort. C and C++ developers typically achieve portability by explicitly adding conditional compilation directives into their application source code using #ifdefs. However, using conditional compilation to address platform-specific variations at all points of use makes it hard to maintain and extend the applica- tion. In particular, the physical design complexity [Lak95] of the application software becomes very high since platform-specific im- plementation details are scattered throughout the application source files. ➥ The readability and maintainability of the code in our logging server example is impeded by the #ifdefs that handle Win32 and Solaris portability. For instance, several #ifdefs are required to handle differences