Chapter 2

Service Access and Configuration Patterns

There once was a man who went to a computer trade show. Each day as he entered, the man told the guard at the door: “I am a great thief, renowned for my feats of shoplifting. Be forewarned, for this trade show shall not escape unplundered.”

This speech disturbed the guard greatly, because there were millions of dollars of computer equipment inside, so he watched the man carefully. But the man merely wandered from booth to booth, asking questions and humming quietly to himself.

When the man left, the guard took him aside and searched his clothes, but nothing was to be found. On the next day of the trade show, the man returned and chided the guard saying: “I escaped with a vast booty yesterday, but today will be even better.” So the guard watched him ever more closely, but to no avail.

On the final day of the trade show, the guard could restrain his curiosity no longer. “Sir Thief”, he said, “I am so perplexed, I cannot live in peace. Please enlighten me. What is it that you are stealing?”

The man smiled. “I am stealing patterns”, he said.

Adapted from “The TAO Of Programming” [JH98] by Geoffrey James and Duke Hillard

This chapter presents four patterns for designing effective application programming interfaces (APIs) to access and configure services and components in stand-alone and networked systems: Wrapper Facade, Component Configurator, Interceptor, and Extension Interface.

Networked systems are inherently heterogeneous [HV99]. Therefore, a key challenge confronting researchers and developers is how to effectively design and configure application access to the interfaces and implementations of evolving service components. This chapter presents four patterns that address various aspects of service access and configuration:

  • The Wrapper Facade design pattern (47) encapsulates the functions and data provided by existing non-object-oriented APIs within more concise, robust, portable, maintainable, and cohesive object-oriented class interfaces. Wrapper Facade is often applied to improve application portability by ‘wrapping’ lower-level operating system APIs. It can also alleviate the accidental complexity associated with programming using low-level APIs.

    To minimize redundancy in other patterns in the book, the Implementation section of the Wrapper Facade pattern contains detailed coverage of wrapper facades for threads, mutex locks, condition variables, and Sockets. Subsequent patterns, such as Reactor (179), Proactor (215), Acceptor-Connector (285), Strategized Locking (333), Active Object (369), and Monitor Object (399), use these wrapper facades in their own implementations. Therefore, we recommend you read Wrapper Facade first.

  • The Component Configurator design pattern (75) allows an application to link and unlink its component implementations at run-time without having to modify, recompile, or relink the application statically. Applications with high availability requirements, such as mission-critical systems that perform on-line transaction processing or real-time industrial process automation, often require such flexible configuration capabilities. Component Configurator therefore addresses aspects of service configuration and service evolution.

    Other patterns in this section, particularly Extension Interface (141) and Interceptor (109), can use the Component Configurator pattern to (re)configure various service roles into components in application processes without having to shut down and restart running application processes.

  • The Interceptor architectural pattern (109) allows services to be added to a framework transparently and to be triggered automatically when certain events occur. Interceptor therefore prepares a framework for its own evolution to accommodate services that are not configured or not even known during the framework’s original development. Interceptor also allows other applications to integrate components and services with instances of the framework. Such services are often ‘out-of-band’ or application-specific from the perspective of the framework instance, but are important for the productive and proper operation of applications that use the framework.
  • The Extension Interface design pattern (141) prevents the ‘bloating’ of interfaces and breakage of client code when developers extend or modify the service functionality of existing components. Multiple extension interfaces can be attached to the same component. Extension Interface addresses both the challenge of component and service evolution and the provision of clients with an authorized and role-specific access to a component’s functionality.

The topics of service access and configuration involve more challenges than are addressed by the patterns in this section. These challenges include:

  • Mediating access to remote services via local proxies
  • Managing the lifecycle of services, locating services in a distributed system and
  • Controlling the operating system and computing resources a server can provide to the service implementations it hosts

Other patterns in the literature address these issues, such as Activator [Stal00], Evictor [HV99], Half Object plus Protocol [Mes95], Locator [JK00], Object Lifetime Manager [LGS99], and Proxy [POSA1] [GoF95]. These patterns complement those presented in this section and together describe key principles that well-structured distributed systems should apply to configure and provide access to the services they offer.

Wrapper Facade

The Wrapper Facade design pattern encapsulates the functions and data provided by existing non-object-oriented APIs within more concise, robust, portable, maintainable, and cohesive object-oriented class interfaces.

Example

Consider a server for a distributed logging service that handles multiple clients concurrently using the connection-oriented TCP protocol [Ste98]. To log data a client must send a connection request to a server transport address, which consists of a TCP port number and IP address. In the logging server, a passive-mode socket handle factory listens on this address for connection requests. The socket handle factory accepts the connection request and creates a data-mode socket handle that identifies this client’s transport address. This handle is passed to the server, which spawns a logging handler thread that processes client logging requests.

After a client is connected it sends logging requests to the server. The logging handler thread receives these requests via its connected socket handle. It then processes the requests in the logging handler thread and writes the requests to a log file.

A common way to develop this logging server is to use low-level C language APIs, such as Solaris threads [EKBF+92] and Sockets [Ste98], to program the server’s threading, synchronization, and network communication functionality. If the logging server runs on multiple platforms, however, there will be differences between functions and data in the low-level APIs, as well as different operating system and compiler features and defects. Developers commonly handle these differences by inserting conditional compilation directives, such as C/C++ #ifdefs, throughout their code. For instance, the following code illustrates a logging server that has been implemented using #ifdefs to run on Solaris and Windows NT:

#if defined (_WIN32)
   #include <windows.h>
   typedef int ssize_t;
#else
   typedef unsigned int UINT32;
   #include <thread.h>
   #include <unistd.h>
   #include <sys/socket.h>
   #include <netinet/in.h>
   #include <memory.h>
#endif /* _WIN32 */
 
// Keep track of number of logging requests.
static int request_count;
 
// Lock that serializes concurrent access to request_count.
#if defined (_WIN32)
   static CRITICAL_SECTION lock;
#else
   static mutex_t lock;
#endif /* _WIN32 */
 
// Maximum size of a logging record.
static const int LOG_RECORD_MAX = 1024;
 
// Port number to listen on for requests.
static const int LOGGING_PORT = 10000;
 
// Entry point that writes logging records.
int write_record (char log_record[], int len) {
   /* … */
   return 0;
}
 
// Entry point that processes logging records for
// one client connection.
#if defined (_WIN32)
   u_long
#else
   void *
#endif /* _WIN32 */
logging_handler (void *arg) {
   // Handle UNIX/Win32 portability.
#if defined (_WIN32)
   SOCKET h = reinterpret_cast <SOCKET> (arg);
#else
   int h = reinterpret_cast <int> (arg);
#endif /* _WIN32 */

   for (;;) { #if defined (_WIN32)    ULONG len; #else    UINT32 len; #endif /* _WIN32 */       // 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’re shutdown or       // errors occur unexpectedly.       if (n <= sizeof len) break;       len = ntohl (len);       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;       } #if defined (_WIN32)       EnterCriticalSection (&lock); #else       mutex_lock (&lock); #endif /* _WIN32 */       // Execute following two statements in a critical       // section to avoid ace conditions and scrambled       // output, respectively.       ++request_count;       // A return value of -1 signifies failure.       if (write_record (log_record, len) == -1)          break; #if defined (_WIN32)       LeaveCriticalSection (&lock); #else       mutex_unlock (&lock); #endif /* _WIN32 */    } #if defined (_WIN32)    closesocket (h); #else    close (h); #endif /* _WIN32 */    return 0; }   // Main driver function for the server. int main (int argc, char *argv[]) {    struct sockaddr_in sock_addr;    // Handle UNIX/Win32 portability. #if defined (_WIN32)    SOCKET acceptor;    WORD version_requested = MAKEWORD(2, 0);    WSADATA wsa_data;    int error = WSAStartup(version_requested, &wsa_data);    if (error != 0) return -1; #else    int acceptor; #endif /* _WIN32 */    // Create a local endpoint of communication.    acceptor = socket (AF_INET, SOCK_STREAM, 0);    // Set up the address to become a server.    memset (&sock_addr, 0, sizeof sock_addr);    sock_addr.sin_family = PF_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 (;;) {       // Handle UNIX/Win32 portability. #if defined (_WIN32)       SOCKET h;       DWORD t_id; #else       int h;       thread_t t_id; #endif /* _WIN32 */       // Block waiting for clients to connect.       h = accept (acceptor, 0, 0);       // Spawn a new thread that runs the <server>       // entry point. #if defined (_WIN32)       CreateThread (0, 0,          LPTHREAD_START_ROUTINE(&logging_handler),          reinterpret_cast <void *> (h), 0, &t_id); #else       thr_create          (0, 0, logging_handler,          reinterpret_cast <void *> (h),          THR_DETACHED, &t_id); #endif /* _WIN32 */    }    return 0; }

The design shown above may work for short-lived, ‘throw-away’ prototypes [FoYo99]. It is inadequate, however, for software that must be maintained and enhanced over time. The use of conditional compilation directives and direct programming of low-level APIs makes the code unnecessarily hard to understand, debug, port, maintain, and evolve.

Certain problems can be alleviated by moving platform-specific declarations, such as the mutex and socket types, into separate configuration header files. This solution is incomplete, however, because the #ifdefs that separate the use of platform-specific APIs, such as thread creation calls, will still pollute application code. Supporting new platforms will also require modifications to platform-specific declarations, irrespective of whether they are included directly into application code or separated into configuration files.

Several well-known patterns address similar problems, but unfortunately do not help to resolve the problems outlined above. For example, Facade [GoF95] encapsulates object-oriented subsystems rather than lower-level non-object-oriented APIs. Decorator [GoF95] extends an object dynamically by attaching additional responsibilities transparently, which incurs unnecessary performance overhead. Bridge and Adapter [GoF95] also introduce an additional layer of indirection that can incur overhead. In general, therefore, these patterns are not well suited to encapsulate existing lower-level non-object oriented APIs, where it may be more important that the solution be efficient than be dynamically extensible.

Context

Maintainable and evolvable applications that access mechanisms or services provided by existing non-object-oriented APIs.

Problem

Applications are often written using non-object-oriented operating system APIs or system libraries. These APIs access network and thread programming mechanisms, as well as user interface or database programming libraries. Although this design is common, it causes problems for application developers by not resolving the following forces:

  • Concise code is often more robust than verbose code because it is easier to develop and maintain. Using object-oriented languages that support higher-level features, such as constructors, destructors, exceptions, and garbage collection, reduces the likelihood of common programming errors. However, developers who program using lower-level function-based APIs directly tend to rewrite a great deal of verbose and error-prone software repeatedly.

    The code for creating and initializing an acceptor socket in the main() function of our logging server example is error-prone. Moreover, these errors are subtle, such as failing to initialize the sock_addr to zero or not using the htons() macro to convert the LOGGING_PORT number into network byte order [Sch92]. The lack of constructors and destructors in C also makes it hard to ensure that resources are allocated and released properly. For example, note how the lock that serializes access to request_count will not be released correctly if the write_record() function returns -1.

  • Software that is portable or can be ported easily to different operating systems, compilers, and hardware platforms helps increase product market-share. Although reusing existing lower-level APIs may reduce some of the software development effort, applications programmed directed with lower-level APIs are often non-portable. Programming using lower-level APIs across different versions of the same operating system or compiler also may be non-portable due to the lack of source-level or binary-level compatibility across software releases [Box97].

    Our logging server example has hard-coded dependencies on several non-portable operating system threading and network programming C APIs. For example, the Solaris thr_create(), mutex_lock(), and mutex_unlock() functions are not portable to Win32 platforms. Although the code is 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 because there is no equivalent to the Solaris THR_DETACHED feature, which spawns a ‘detached’ thread whose exit status is not retained by the threading library [Lew95].

  • Improving software maintainability helps reduce lifecycle costs. Programs written directly to low-level non-object-oriented APIs are often hard to maintain, however. For example, C and C++ developers often address portability issues by embedding conditional compilation directives into their application source. Unfortunately, addressing platform-specific variations via conditional compilation at all points of use increases the software’s physical design complexity [Lak95]. For instance, platform-specific details become scattered throughout the application source files.

    Maintenance of our logging server is impeded by the #ifdefs that handle Win32 and Solaris portability, for example the differences in the type of a socket on Win32 and Solaris. In general, developers who program to low-level C APIs like these must have intimate knowledge of many operating system idiosyncrasies to maintain and evolve their code.

  • Cohesive components are easier to learn, maintain, and enhance. However, low-level APIs are rarely grouped into cohesive components because languages like C lack features such as classes, namespaces, or packages. It is hard, therefore, to recognize the extent of low-level APIs. Programming with non-cohesive stand-alone function APIs also scatters common code throughout an application, making it hard to ‘plug in’ new components that support different policies and mechanisms.

    The Socket API is particularly hard to learn because the several dozen C functions in the Socket library lack a uniform naming convention. For example, it is not obvious that socket(), bind(), listen(), connect(), and accept() are related. Other low-level network programming APIs, such as TLI, address this problem by prepending a common function prefix, such as the t_prefixed before each function in the TLI API. However, the use of a common prefix does not by itself make the TLI API more ‘pluggable’ than Sockets. It remains a low-level function-based API rather than a more cohesive object-oriented class interface.

In general, developing applications by programming to non-object-oriented APIs directly is a poor design choice for software that must be maintained and evolved over time.

Solution

Avoid accessing non-object-oriented APIs directly. For each set of related functions and data in a non-object-oriented API, create one or more wrapper facade classes that encapsulate these functions and data within the more concise, robust, portable, and maintainable methods provided by the object-oriented wrapper facade(s).

Structure

There are two participants in the Wrapper Facade pattern:

Functions are the building blocks of existing non-object-oriented APIs. They provide a stand-alone service or mechanism and manipulate data passed as parameters or accessed through global variables.

A wrapper facade is a set of one or more object-oriented classes that encapsulate existing functions and their associated data. These class(es) export a cohesive abstraction that provides a specific type of functionality. Each class represents a specific role in this abstraction.

The methods in the wrapper facade class(es) generally forward application invocations to one or more of the functions, passing the data as parameters. The data is often hidden within the private portion of the wrapper facade and is not accessible to client applications. Compilers can then enforce type safety because primitive data types, such as pointers or integers, are encapsulated within strongly-typed wrapper facades.

The following class diagram illustrates the structure of Wrapper Facade:

Dynamics

Collaborations in the Wrapper Facade pattern are often straightforward:

  • The application code invokes a method on an instance of the wrapper facade.
  • The wrapper facade method forwards the request and its parameters to one or more of the lower-level API functions that it encapsulates, passing along any internal data needed by the underlying function(s).

Implementation

This section describes the activities involved in implementing the Wrapper Facade pattern. Certain activities may require multiple iterations to identify and implement suitable wrapper facade abstractions. To reduce repetition elsewhere in the book, we present an in-depth discussion of concrete wrapper facades for mutexes, condition variables, Sockets, and threads in this section. Although this lengthens the current section somewhat, the implementation examples of other patterns in this book, including Acceptor-Connector (285), Strategized Locking (333), Thread-Specific Storage (475), and Monitor Object (399), are simplified by using these wrapper facades.

1 Identify the cohesive abstractions and relationships among existing low-level APIs. Mature low-level APIs contain functions and data structures that define many cohesive abstractions and map cleanly onto object-oriented classes and methods. Common examples include the C APIs for Win32 synchronization and threading, POSIX network programming, and X Windows GUI event dispatching. Due to the lack of data abstraction in languages like C, however, it may not be clear how functions in these existing APIs relate to each other. The first activity in implementing the Wrapper Facade pattern is therefore to identify the cohesive abstractions and relations among existing APIs.

The original implementation of our logging server carefully uses many low-level functions that provide several cohesive operating system mechanisms, such as synchronization and network communication. The Solaris mutex_lock() and mutex_unlock() functions, for example, are associated with a mutex synchronization abstraction. Similarly, the socket(), bind(), listen(), and accept() functions play various roles in a network programming abstraction.

If existing functions and data structures have been developed as throw-away code or via piecemeal growth [FoYo99], they may exhibit little or no cohesive abstractions. In this case the code should be refactored [Opd92] [FBBOR99], if possible, before proceeding with the implementation of the Wrapper Facade pattern.

2 Cluster cohesive groups of functions into wrapper facade classes and methods. This activity defines one or more class abstractions that shield applications from low-level data representations, arbitrary variations in function syntax, and other implementation details. It can be decomposed into five sub-activities:
2.1 Create cohesive classes. We start by defining one or more wrapper facade classes for each group of existing non-object-oriented APIs that are related to a particular abstraction. Common criteria used to create cohesive classes include the following:
  • Coalesce functions and data with high cohesion into individual classes, while minimizing unnecessary coupling between classes. Examples of cohesive functions are those that manipulate common data structures, such as a Socket, a file, or a signal set [Ste98].
  • Identify the common and variable aspects in the underlying functions and data [Cope98]. Common aspects include mechanisms for synchronization, threading, memory management, addressing, and operating system platform APIs. Variable aspects often include the implementations of these mechanisms. Whenever possible variation in functions and data should be factored into classes that isolate variation behind uniform interfaces to enhance extensibility.

In general, if the original API contains a wide range of related functions, it may be necessary to create several wrapper facade classes to separate concerns properly.

2.2 Coalesce multiple individual functions into a single method. In addition to grouping existing functions into classes, it may be useful to coalesce multiple individual functions into a smaller number of methods in each wrapper facade class. Coalescing can be used to ensure that a group of lower-level functions are called in the appropriate order, as with the Template Method pattern [GoF95].
2.3 Automate creation and destruction operations, if possible. Lower-level APIs often require programmers to call functions explicitly to create and destroy data structures that implement instances of the API. This procedure is error-prone, however, because developers may forget to call these functions in one or more paths through their code. A more robust approach therefore is to leverage the implicit creation and destruction operation capabilities provided by object-oriented languages, such as C++ and Java. In fact, the ability to create and destroy objects automatically often justifies the use of the Wrapper Facade pattern, even if the wrapper facade methods do nothing but forward control to the lower-level function calls.
2.4 Select the level of indirection. Most wrapper facade classes simply forward their method calls to the underlying low-level functions, as mentioned above. If wrapper facade methods can be inlined implicitly or explicitly, there need be no run-time indirection overhead when compared to invoking the low-level functions directly. It is also possible to add another level of indirection by dispatching wrapper facade implementations using dynamically bound methods or some other form of polymorphism. In this case the wrapper facade classes play the role of the abstraction class in the Bridge pattern [GoF95].
2.5 Determine where to encapsulate any platform-specific variation. A common use of the Wrapper Facade pattern is to minimize platform-specific variation in application code. Although wrapper facade method implementations may differ across different operating system platforms, they should provide uniform, platform-independent interfaces. Where platform-specific variation exists it can be encapsulated via conditional compilation or separate directories:
  • Conditional compilation can be used to select among different wrapper facade class method implementations. The use of conditional compilation is inelegant and tedious when #ifdefs are scattered throughout application code. Conditional compilation may be acceptable, however, if it is localized in a few platform-specific wrapper facade classes or files that are not accessed directly by application developers. When conditional compilation is used in conjunction with auto-configuration tools, such as GNU autoconf, platform-independent wrapper facades can be created within a single source file. As long as the number of variations supported in this file is not unwieldy therefore, conditional compilation can help localize variation and simplify maintenance.
  • Separate directories can be used to factor out different wrapper facade implementations, thereby minimizing conditional compilation or avoiding it altogether. For example, each operating system platform can have its own directory containing implementations of platform-specific wrapper facades. Language processing tools can be used to include the appropriate wrapper facade class from the relevant directory at compilation. To obtain a different implementation, a different include path could be provided to the compiler. This strategy avoids the problems with conditional compilation described above because it physically decouples the various alternative implementations into separate directories.

Choosing a strategy depends on how often wrapper facade interfaces and implementations change. If changes occur frequently it may be time-consuming to update the conditional compilation sections for each platform. Similarly, all files that depend on the affected files will be recompiled even if the change is only necessary for one platform. Therefore the use of condition compilation becomes increasingly complex as a larger number of different platforms are supported. Regardless of which strategy is selected, however, the burden of maintaining wrapper facade implementations should be the responsibility of wrapper facade developers rather than application developers.

To simplify our logging server implementation, we define wrapper facades that encapsulate existing low-level C APIs for mutexes, Sockets, and threads. Each wrapper facade illustrates how various design issues outlined above can be addressed systematically. We focus on defining wrapper facades for C functions because C is used to define popular operating system APIs, such as POSIX or Win32. However, the same design principles and techniques can be applied to other non-object-oriented languages, such as FORTRAN, Ada 83, Scheme, or Pascal, as well as to non-operating system APIs, such as X Windows or ODBC database toolkits [San98].

Mutex wrapper facades. We first define a Thread_Mutex abstraction that encapsulates the Solaris mutex functions with a uniform and portable class interface:1

class Thread_Mutex {
public:
   Thread_Mutex ()
      { mutex_init (&mutex_, USYNC_THREAD, 0); }
   ~Thread_Mutex () { mutex_destroy (&mutex_); }
 
   void acquire () { mutex_lock (&mutex_); }
   void release () { 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 &);
 
   // Define a <Thread_Condition> as a friend so it can
   // access <mutex_>.
   friend class Thread_Condition;
};

Note how we define the copy constructor and assignment operator as private methods in the Thread_Mutex class. This C++ idiom ensures that application programmers cannot copy or assign one Thread_Mutex to another accidentally [Mey98] [Str97]. Copying mutexes is a semantically-invalid operation that is erroneously permitted by the less strongly-typed C programming API. Our Thread_Mutex wrapper facade therefore provides a mutex interface that is less error-prone than programming directly to the lower-level Solaris synchronization functions.

By defining a Thread_Mutex class interface and then writing applications to use it, rather than lower-level native operating system C APIs, we can port our wrapper facade to other platforms more easily. For example, the identical Thread_Mutex interface can be implemented to run on Win32:

class Thread_Mutex {
public:
   Thread_Mutex ()
      { InitializeCriticalSection (&mutex_); }
   ~Thread_Mutex ()
      { DeleteCriticalSection (&mutex_); }
 
   void acquire () { EnterCriticalSection (&mutex_); }
   void release () { LeaveCriticalSection (&mutex_); }
private:
   // Win32-specific Mutex mechanism.
   CRITICAL_SECTION mutex_;
 
   // Disallow copying and assignment.
   Thread_Mutex (const Thread_Mutex &);
   void operator= (const Thread_Mutex &);
};

Naturally, a complete implementation of Thread_Mutex would map the platform-specific error handling return values from the various mutex_t and CRITICAL_SECTION functions to portable C++ exceptions.

As described earlier, we can support multiple operating systems simultaneously by using conditional compilation and #ifdef’ing the Thread_Mutex method implementations. If conditional compilation is unwieldy due to the number of supported platforms, it is possible to factor out the different Thread_Mutex implementations into separate directories. In this case, language processing tools such as compilers and preprocessors can be instructed to include the appropriate platform-specific variant into the application during compilation.

Condition variable wrapper facade. A condition variable is a synchronization mechanism used by collaborating threads to suspend themselves temporarily until condition expressions involving data shared between the threads attain desired states [IEEE96]. We describe the wrapper facade for condition variables at this point because they are often used in conjunction with the Thread_Mutex wrapper facade described above. Although our logging server example does not use condition variables, they are used by other patterns throughout the book, such as Strategized Locking (333), Leader/Followers (447), and Monitor Object (399).

As mentioned above, a condition variable is always used in conjunction with a mutex that the client thread must acquire before evaluating the condition expression. If the condition expression is false the client suspends itself on the condition variable and releases the mutex atomically, so that other threads can change the shared data. When a cooperating thread changes this data it can notify the condition variable, which resumes a thread atomically that had suspended itself previously on the condition variable. The thread then re-acquires the mutex associated with the condition variable.

After re-acquiring its mutex a newly-resumed thread next re-evaluates its condition expression. If the shared data has attained the desired state, the thread continues. Otherwise it suspends itself on the condition variable again until it is resumed. This process can repeat until the condition expression becomes true.

In general, when complex condition expressions or scheduling behaviors are required, combining a mutex with a condition variable is more appropriate than just using a mutex. For example, condition variables can be used to implement synchronized message queues, as shown in the Monitor Object pattern example (399). In this situation a pair of condition variables are employed to block supplier threads cooperatively when a message queue is full, and to block consumer threads when the queue is empty.

The following Thread_Condition class is a wrapper facade that is implemented using the Solaris condition variable API:

class Thread_Condition {
public:
   // Initialize the condition variable and
   // associate it with the &ltmutex_>.
   Thread_Condition (const Thread_Mutex &m) : mutex_ (m)
      { cond_init (&cond_, USYNC_THREAD, 0); }
 
   // Destroy the condition variable.
   ~Thread_Condition () { cond_destroy (&cond_); }
 
   // Wait for the <Thread_Condition> to be notified
   // or until <timeout> has elapsed. If <timeout> == 0
   // then wait indefinitely.
   void wait (Time_Value *timeout = 0) {
      cond_timedwait (&cond_, &mutex_.mutex_,
               timeout == 0
                  ? 0 : timeout->msec ());
}
   // Notify one thread waiting on <Thread_Condition>.
   void notify () { cond_signal (&cond_); }
 
   // Notify all threads waiting on <Thread_Condition>.
   void notify_all () { cond_broadcast (&cond_); }
private:
   // Solaris condition variable.
   cond_t cond_;
 
   // Reference to mutex lock.
   const Thread_Mutex &mutex_;
};

The constructor initializes the condition variable and associates it with the Thread_Mutex passed as a parameter. The destructor destroys the condition variable, which releases allocated resources. Note that the mutex is not owned by the Thread_Condition so it is not destroyed in the destructor.

When called by a client thread the wait() method performs the following two steps atomically:

  • It releases the associated mutex and
  • It suspends itself atomically for up to a timeout amount of time, waiting for the Thread_Condition object to be notified by another thread.

The notify() method resumes one thread waiting on a Thread_Condition. Similarly the notify_all() method notifies all threads that are currently waiting on a Thread_Condition. The mutex_ lock is reacquired by the wait() method before it returns to its client thread, either because the condition variable was notified or because its timeout expired.

Socket wrapper facades. Our next wrapper facade encapsulates the Socket API. This API is much larger and more expressive than the Solaris mutex API [Sch92]. We must therefore define a group of related wrapper facade classes to encapsulate Sockets. We start by defining a typedef and a macro that hide some of the UNIX/POSIX and Win32 portability differences:

typedef int SOCKET;
const int INVALID_HANDLE_VALUE = -1;

Both SOCKET and INVALID_HANDLE_VALUE are defined in the Win32 API already. Therefore, we could either integrate them using #ifdefs or using separate platform-specific directories, as discussed earlier in implementation activity 2.5 (55).

Next, we define an INET_Addr class that encapsulates the internet domain address struct:

Next, we define an INET_Addr class that encapsulates the internet domain address struct:

class INET_Addr {
public:
   INET_Addr (u_short port, u_long addr) {
      // Set up the address to become a server.
      memset (&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 () const { return addr_.sin_port; }
 
   u_long get_ip_addr () const
      { return addr_.sin_addr.s_addr; }
 
   sockaddr *addr () const
      { return reinterpret_cast <sockaddr *> (&addr_);}
 
   size_t size () const { return sizeof (addr_); }
 
   // …
private:
   sockaddr_in addr_;
};

Note how the INET_Addr constructor eliminates several common Socket programming errors. For example, it initializes the sockaddr_in field to zero, and ensures the TCP port number and IP address are converted into network byte order by applying the ntons() and ntonl() macros automatically [Ste98].

The next wrapper facade class, SOCK_Stream, encapsulates the I/O operations, such as recv() and send(), that an application can invoke on a connected socket handle:

class SOCK_Stream {
public:
   // Default and copy constructor.
   SOCK_Stream () : handle_ (INVALID_HANDLE_VALUE) { }
   SOCK_Stream (SOCKET h): handle_ (h) { }
 
   // Automatically close the handle on destruction.
   ~SOCK_Stream () { close (handle_); }
   // Set/get the underlying SOCKET handle.
   void set_handle (SOCKET h) { handle_ = h; }
   SOCKET get_handle () const { return handle_; }
 
   // Regular I/O operations.
   ssize_t recv (void *buf, size_t len, int flags);
   ssize_t send (const char *buf, size_t len, int flags);
 
   // I/O operations for “short” receives and sends.
   ssize_t recv_n (char *buf, size_t len, int flags);
   ssize_t send_n (const char *buf, size_t len, int flags);
 
 
   // … other methods omitted.
private:
   // Socket handle for exchanging socket data.
   SOCKET handle_;
};

As discussed in implementation activity 2.3 (55), this class leverages the semantics of C++ destructors to ensure that a socket handle is closed automatically when a SOCK_Stream object goes out of scope. In addition, the send_n() and recv_n() methods can handle networking idiosyncrasies, for example ‘short’ send and receive operations.

SOCK_Stream objects can be created via a connection factory, called SOCK_Acceptor, which encapsulates passive establishment of Socket connections. The SOCK_Acceptor constructor initializes the passive-mode acceptor socket to listen at the sock_addr address. The SOCK_Acceptor’s accept() method is a factory that initializes the SOCK_Stream parameter with a socket handle to a new connection:

class SOCK_Acceptor {
public:
   // Initialize a passive-mode acceptor socket.
   SOCK_Acceptor (const INET_Addr &addr) {
      // Create a local endpoint of communication.
      handle_ = socket (PF_INET, SOCK_STREAM, 0);
      // Associate address with endpoint.
      bind (handle_, addr.addr (), addr.size ());
      // Make endpoint listen for connections.
      listen (handle_, 5);
   };
 
   // A second method to initialize a passive-mode
   // acceptor socket, analogously to the constructor.
   void open (const INET_Addr &sock_addr) { /* … */ };
   // Accept a connection and initialize the <stream>.
   void accept (SOCK_Stream &s) {
      s.set_handle (accept (handle_, 0, 0));
   }
private:
   SOCKET handle_; // Socket handle factory.
};

Note how the constructor for the SOCK_Acceptor applies the strategy discussed in implementation activity 2.2 (55) to ensure that the low-level socket(), bind(), and listen() functions are always called together and in the correct order.

A complete set of Socket [Sch97] wrapper facades would also include a SOCK_Connector that encapsulates the logic for establishing connections actively. The SOCK_Acceptor and SOCK_Connector classes are concrete IPC mechanisms that can be used to instantiate the generic acceptor and connector classes described in the Acceptor-Connector pattern (285) to perform connection establishment.

Thread wrapper facade. Our final wrapper facade encapsulates operating system threading APIs that are available on different operating system platforms, including Solaris threads, POSIX Pthreads, and Win32 threads. These APIs exhibit subtle syntactic and semantic differences. For example, Solaris and POSIX threads can be spawned in ‘detached’ mode, whereas Win32 threads cannot. It is possible, however, to provide a Thread_Manager wrapper facade that encapsulates these differences in a uniform manner. The Thread_Manager wrapper facade below, which is a Singleton [GoF95], illustrates the spawn method implemented for Solaris threads:

class Thread_Manager {
public:
   // Singleton access point.
   Thread_Manager *instance ();
 
   // Spawn a thread.
   void spawn (void *(*entry_point_function) (void *),
            void *arg = 0, long flags = 0,
            long stack_size = 0,
            void *stack_pointer = 0,
            thread_t *t_id = 0) {
      thread_t t;
      if (t_id == 0)
         t_id = &t;
         thr_create
            (stack_size, stack_pointer,
            entry_point_function, arg, flags, t_id);
      }
      // … Other methods omitted.
};

The Thread_Manager class also provides methods for joining and canceling threads that can be ported to other operating systems.

3 Consider allowing applications controlled access to implementation details. One benefit of defining a wrapper facade is to make it hard to write incorrect or non-portable applications. For example, wrapper facades can shield applications from error-prone or platform-specific implementation details, such as whether a socket is represented as a pointer or an integer. Cases may arise, however, where the extra abstraction and type safety actually prevent programmers from using a wrapper facade in useful ways not anticipated by its designer. This experience can be frustrating and may discourage programmers from leveraging other benefits of wrapper facades.

A common solution to the problem of ‘too much’ abstraction is to provide an ‘escape hatch’ mechanism or open implementation technique, such as AOP [KLM+97]. This design allows applications to access implementation details in a controlled manner.

The SOCK_Stream class defines a pair of methods that set and get the underlying SOCKET handle:

class SOCK_Stream {
public:
   // Set/get the underlying SOCKET handle.
   void set_handle (SOCKET h) { handle_ = h; }
   SOCKET get_handle () const { return handle_; }

These methods can be used to set and get certain Socket options, such as support for ‘out-of-band’ data [Ste98], that were not defined by the original Socket wrapper facades.

Escape-hatch mechanisms should be used sparingly of course, because they decrease portability and increase the potential for errors, thereby nullifying key benefits of the Wrapper Facade pattern. If applications use certain escape hatches repeatedly in similar situations, it may indicate that explicit methods should be added to the public interface of the wrapper facade. The Extension Interface pattern (141) defines techniques for adding these new methods without disrupting existing clients.

4 Develop an error-handling mechanism. Low-level C operating system function APIs often use return values and integer codes, such as errno, to return errors to their calling code. This technique can be error-prone, however, if callers do not check the return status of their function calls.

A more elegant way of reporting errors is to use exception handling. Many programming languages, such as C++ and Java, support exception handling as a fundamental error-reporting mechanism. It is also used in some operating systems, for example Win32 [Sol98]. There are several benefits of using exception handling as the error-handling mechanism for wrapper facade classes:

  • It is extensible, for example by defining hierarchies of exception classes in C++ and Java.
  • It cleanly decouples error handling from normal processing. Error handling information is neither passed to an operation explicitly, nor can an application accidentally ignore an exception by failing to check function return values.
  • It can be type-safe. In languages like C++ and Java, exceptions can be thrown and caught in a strongly-typed manner.

We can define the following exception class to keep track of which operating system error or condition has occurred:

class System_Ex : public exception {
public:
   // Map <os_status> into a platform-independent error
   // or condition status and store it into <error_>.
   System_Ex (int os_status) { /* … */ }
 
   // Platform-independent error or condition status.
   int status () const { return status_; }
   // …
private:
   // Store platform-independent error/condition status.
   int status_;
};

Platform-independent errors and conditions could be defined via macros or constants that map onto unique values across all operating systems. For instance, the Solaris implementation of the Thread_Mutex::acquire() method shown on page 57 could be written as follows:

void Thread_Mutex::acquire () {
   int result = mutex_lock (&mutex);
   if (result != 0) { throw System_Ex (result); }
}

Unfortunately, there are several drawbacks to the use of exception handling for wrapper facade classes:

  • Not all languages or implementations provide exception handling. For example, C does not define an exception model and some C++ compilers do not implement exceptions.
  • Languages implement exceptions in different ways. It can thus be hard to integrate components written in different languages when they throw exceptions. Using proprietary exception handling mechanisms, such as Windows NT’s structured exception handling [Sol98], can also reduce the portability of applications that use these mechanisms.
  • Resource management can be complicated if there are multiple exit paths from a block of C++ or Java code [Mue96]. If garbage collection is not supported by the language or programming environment, care must be taken to ensure that dynamically-allocated objects are deleted when an exception is thrown.
  • Poor exception handling implementations incur time or space overheads even when exceptions are not thrown [Mue96]. This overhead is problematic for embedded systems that must be efficient and have small memory footprints [GS98].

The drawbacks of exception handling are particularly problematic for wrapper facades that encapsulate kernel-level device drivers or low-level operating system APIs that must run on many platforms [Sch92], such as the mutex, Socket and thread wrapper facades described above. A common error handling mechanism for system-level wrapper facades [Sch97] is based on the Thread-Specific Storage pattern (475) in conjunction with errno. This solution is efficient, portable, and thread-safe, though more obtrusive and potentially error-prone than using C++ exceptions.

5 Define related helper classes (optional). After lower-level APIs are encapsulated within wrapper facade classes it often becomes possible to create other helper classes that further simplify application development. The benefits of these helper classes are often apparent only after the Wrapper Facade pattern has been applied to cluster lower-level functions and their associated data into classes.

In our logging example we can leverage the Guard template class defined in the Strategized Locking pattern (333) [Str97]. This class ensures that a Thread_Mutex is acquired and released properly within a scope regardless of how the method’s flow of control leaves the scope. The Guard class constructor acquires the mutex and the destructor releases it within a scope automatically:

{
   // Constructor of <guard> automatically
   // acquires the <mutex> lock.
   Guard<Thread_Mutex> guard (mutex);
   // … operations that must be serialized.
 
   // Destructor of <guard> automatically
   // releases the <mutex> lock.
}

We can easily substitute a different type of locking mechanism while still using the Guard’s automatic locking and unlocking protocol because we used a class as the Thread_Mutex wrapper facade. For example, we can replace the Thread_Mutex class with a Process_Mutex class:

// Acquire a process-wide mutex.
Guard<Process_Mutex> guard (mutex);

It is much harder to achieve this degree of ‘pluggability’ using lower-level C functions and data structures instead of C++ classes. The main problem is that the functions and data lack language support for cohesion, whereas the C++ classes provide this support naturally.

Example Resolved

The code below illustrates the logging_handler() function of our logging server after it has been rewritten to use the wrapper facades for mutexes, Sockets, and threads described in the Implementation section. To ease comparison with the original code, we present it in a two-column table with the original code from the example section in the left-hand column and the new code in the right-hand column The code in the right-hand column addresses the problems with the code shown in the left-hand column. For example, the destructors of SOCK_Stream and Guard will close the socket handle and release the Thread_Mutex, respectively, regardless of how the code blocks are exited. This code is also easier to understand, maintain, and port because it is more concise and uses no platform-specific APIs.

#if defined (_WIN32)
   #include <windows.h>
   typedef int ssize_t;
#else
   typedef unsigned int UINT32;
   #include <thread.h>
   #include <unistd.h>
   #include <sys/socket.h>
   #include <netinet/in.h>
   #include <memory.h>
#endif /* _WIN32 */
 
// Keep track of number of logging requests.
static int request_count;
 
// Lock to protect request_count.
#if defined (_WIN32)
   static CRITICAL_SECTION lock;
#else
   static mutex_t lock;
#endif /* _WIN32 */
 
// Maximum size of a logging record.
static const int LOG_RECORD_MAX = 1024;
 
// Port number to listen on for requests.
static const int LOGGING_PORT = 10000;
 
// Entry point that writes logging records.
int write_record (const char log_record[], size_t len) {
   /* … */ return 0;
}
 
// Entry point that processes logging records for
// one client connection.
#if defined (_WIN32)
   u_long
#else
   void *
#endif /* _WIN32 */
logging_handler (void *arg) {
#if defined (_WIN32)
   SOCKET h = reinterpret_cast <SOCKET> (arg);
#else
   int h = reinterpret_cast <int> (arg);
#endif /* _WIN32 */
 
   for (;;) {
      // Ensure a 32-bit quantify;
#if defined (_WIN32)
      ULONG len;
#else
      UINT32 len;
#endif /* _WIN32 */
            char log_record[LOG_RECORD_MAX];
 
      // The first <recv> reads the length
      // (stored as a 32-bit integer) of
      // adjacent logging record.
      ssize_t n = recv (h, &len, sizeof len, 0);
 
      if (n <= sizeof len) break; // Bailout on error.
      len = ntohl (len);
      if (len > LOG_RECORD_MAX) break;
      // Loop to <recv> the data.
      for (size_t nread = 0; nread < len; nread += n) {
         n = recv (h, log_record + nread,
               len - nread, 0);
         if (n <= 0) return 0;
      }
#if defined (_WIN32)
      EnterCriticalSection (&lock);
#else
      mutex_lock (&lock);
#endif /* _WIN32 */
      ++request_count;
      // A -1 return value signifies failure.
      if (write_record (log_record, len) == -1)
         break;
#if defined (_WIN32)
      LeaveCriticalSection (&lock);
#else
      mutex_unlock (&lock);
#endif /* _WIN32 */
   }
#if defined (_WIN32)
   closesocket (h);
#else
   close (h);
#endif /* _WIN32 */
   return 0;
}
#include “ThreadManager.h”
#include “ThreadMutex.h”
#include “Guard.h”
#include “INET_Addr.h”
#include “SOCKET.h”
#include “SOCK_Acceptor.h”
#include “SOCK_Stream.h”
 
 
 
 
// Keep track of number of logging requests.
static int request_count;
 
 
 
 
 
// Maximum size of a logging record.
static const int LOG_RECORD_MAX = 1024;
 
// Port number to listen on for requests.
static const int LOGGING_PORT = 10000;
 
// Entry point that writes logging records.
int write_record (const char log_record[], size_t len) {
   /* … */ return 0;
}
 
// 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.
   SOCK_Stream stream (h);
   for (;;) {
      // Ensure a 32-bit quantity.
      UINT_32 len;
 
 
 
 
      char log_record[LOG_RECORD_MAX];
 
      // The first <recv_n> reads the length
      // (stored as a 32-bit integer) of
      // adjacent logging record.
      ssize_t n = stream.recv_n (&len, sizeof len);
 
      if (n <= 0) break; // Bailout on error.
      len = ntohl (len);
      if (len > LOG_RECORD_MAX) break;
      // Second <recv_n> reads the data.
      n = stream.recv_n (log_record, len);

      if (n <= 0) break;       {          // Constructor acquires the lock.          Guard<Thread_Mutex> mon (lock);                ++request_count;          // A -1 return value signifies failure.          if (write_record (log_record, len) == -1)             break;            // Destructor releases the lock.       }    }            return 0;    // Destructor of <stream> closes down <h>. }

Analogously to the logging_handler() function, we present a two-column table below that compares the original code for the main() function with the new code using wrapper facades:

// Main driver function for the server.
int main (int argc, char *argv[]) {
   struct sockaddr_in sock_addr;
   // Handle UNIX/Win32 portability.
#if defined (_WIN32)
   SOCKET acceptor;
   WORD version_requested = MAKEWORD(2, 0);
   WSADATA wsa_data;
   int error = WSAStartup(version_requested, &wsa_data);
   if (error != 0) return -1;
#else
   int acceptor;
#endif /* _WIN32 */
   // Create a local endpoint of communication.
   acceptor = socket (AF_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 = PF_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 (;;) {
      // Handle UNIX/Win32 portability.
#if defined (_WIN32)
      SOCKET h;
      DWORD t_id;
#else
      int h;
      thread_t t_id;
#endif /* _WIN32 */
      // Block waiting for clients to connect.
      h = accept (acceptor, 0, 0);
 
 
 
      // Spawn a new thread that runs the <server>
      // entry point.
#if defined (_WIN32)
      CreateThread (0, 0,
         LPTHREAD_START_ROUTINE(&logging_handler),
         reinterpret_cast <void *> (h), 0, &t_id);
#else
      thr_create
         (0, 0, logging_handler,
         reinterpret_cast <void *> (h),
         THR_DETACHED, &t_id);
#endif /* _WIN32 */
   }
   return 0;
}
// Main driver function for the server.
int main (int argc, char *argv[]) {
      INET_Addr addr (port);
 
      // Passive-mode acceptor object.
      SOCK_Acceptor server (addr);
      SOCK_Stream new_stream;
      // Main server event loop.
 
 
 
 
 
 
 
 
 
 
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);
   }
 
 
 
 
   return 0;
}

Note how literally dozens of lines of low-level, conditionally compiled code disappear in the right-hand column version that uses the Wrapper Facade pattern.

Known Uses

Microsoft Foundation Classes (MFC). MFC [Pro99] provides a set of wrapper facades that encapsulate many lower-level C Win32 APIs. It focuses largely on providing GUI components that implement the Microsoft Document-View architecture, which is a variant of the Document-View architecture described in [POSA1].

ACE. The Socket, thread, and mutex wrapper facades described in the Implementation section are abstractions of ACE framework [Sch97] components, such as the ACE_SOCK*, ACE_Thread_Manager and ACE_ Thread_Mutex classes, respectively.

Rogue Wave. Rogue Wave’s Net.h++ and Threads.h++ class libraries implement wrapper facades for Sockets, threads, and synchronization mechanisms on a number of operating system platforms.

ObjectSpace. The ObjectSpace System<Toolkit> also implements platform-independent wrapper facades for Sockets, threads, and synchronization mechanisms.

Java Virtual Machine and Java class libraries. The Java Virtual Machine (JVM) and various Java class libraries, such as AWT and Swing [RBV99], provide a set of wrapper facades that encapsulate many low-level native operating system calls and GUI APIs.

Siemens REFORM. The REFORM framework [BGHS98] for hot rolling mill process automation uses the Wrapper Facade pattern to shield the object-oriented parts of the system, such as material tracking and setpoint transmission, from a neural network for the actual process control. This neural network is programmed in C due to its algorithmic nature and contains mathematical models that characterize the physics of the automation process.

The wrapper facades defined in the REFORM framework differ from wrapper facades for operating system mechanisms because the process-control APIs they encapsulate are at a higher level of abstraction. In fact the neural network is part of the REFORM system itself. However, its function-based C APIs are lower-level compared to the complex object-oriented high-level structure and logic of the hot rolling mill framework. The REFORM wrapper facades therefore have similar goals and properties as the lower-level operating system wrapper facades:

  • They provide the views and abstractions that the object-oriented parts of the framework need of the process control neural network. There is a separate wrapper facade for every component using the neural network.
  • They hide API variations. For different customer-specific instances of the framework there may be (slightly) different implementations of the neural network. As a result, semantically identical functions in these neural network implementations may have different signatures. These differences do not affect the framework implementation, however.
  • They ensure lower-level C functions are invoked in the right order.

Books consisting of edited collections of papers. A real-life example of the Wrapper Facade pattern are books consisting of edited collections of papers that are organized into one or more ‘themes’. For example, the PLoPD series [PLoPD1] [PLoPD2] [PLoPD3] [PLoPD4] consist of individual papers that are organized into cohesive sections, such as event handling, fault tolerance, application framework design, or concurrency. Thus, readers who are interested in a particular topic area or domain can focus their attention on these sections, rather than having to locate each paper individually.

Consequences

The Wrapper Facade pattern provides the following benefits:

Concise, cohesive and robust higher-level object-oriented programming interfaces. The Wrapper Facade pattern can be used to encapsulate lower-level APIs within a more concise and cohesive set of higher-level object-oriented classes. These abstractions reduce the tedium of developing applications, thereby decreasing the potential for certain types of programming error. In addition, the use of encapsulation eliminates programming errors that occur when using untyped data structures incorrectly, such as socket or file handles. Application code can therefore use wrapper facades to access lower-level APIs correctly and uniformly.

Portability and maintainability. Wrapper facades can be implemented to shield application developers from non-portable aspects of lower-level APIs. The Wrapper Facade pattern also improves software structure by replacing an application configuration strategy based on physical design entities, such as files and #ifdefs, with logical design entities, such as base classes, subclasses, and their relationships. It is often much easier to understand, maintain, and enhance applications in terms of their logical design rather than their physical design [Lak95].

Modularity, reusability and configurability. The Wrapper Facade pattern creates cohesive and reusable class components that can be ‘plugged’ into other components in a wholesale fashion, using object-oriented language features like inheritance and parameterized types. In contrast, it is harder to replace groups of functions without resorting to coarse-grained operating system tools such as linkers or file systems.

The Wrapper Facade pattern incurs several liabilities:

Loss of functionality. Whenever an abstraction is layered on top of an existing abstraction it is possible to lose functionality. In particular, situations can occur in which the new abstraction prevents developers from accessing certain capabilities of the underlying abstraction. It is hard to define a suitable high-level abstraction that covers all these use cases without becoming bloated. One useful heuristic to follow is to design wrapper facades so that they are easy to use correctly, hard to use incorrectly, but not impossible to use in ways that the original designers did not anticipate. An ‘escape-hatch’ mechanism or open implementation [KLM+97] technique can often help reconcile these design forces cleanly.

Performance degradation. The Wrapper Facade pattern can degrade performance. For example, if wrapper facade classes are implemented with the Bridge pattern [GoF95], or if they make several forwarding function calls per method, the additional indirection may be more costly than programming to the lower-level APIs directly. However, languages that support inlining, such as C++ or certain C compilers, can implement the Wrapper Facade pattern with no significant overhead, because compilers can inline the method calls used to implement the wrapper facades. The overhead is therefore the same as calling lower-level functions directly.

Programming language and compiler limitations. Defining C++ wrapper facades for well-designed C APIs is relatively straightforward, because the C++ language and C++ compilers define features that facilitate cross-language integration. It may be hard to define wrapper facades for other languages, however, due to a lack of language support or limitations with compilers. For example, there is no universally accepted standard for integrating C functions into languages like Ada, Smalltalk, and Java. Programmers may therefore need to use to non-portable mechanisms to develop wrapper facades.

See Also

The Wrapper Facade pattern is related to several of the structural patterns in [GoF95], including Facade, Bridge, Adapter, and Decorator.

Facade. The intent of Facade is to provide a unified interface that simplifies client access to subsystem interfaces. The intent of Wrapper Facade is more specific: it provides concise, robust, portable, maintainable, and cohesive class interfaces that encapsulate lower-level APIs such as operating system mutex, Socket, thread, and GUI C APIs. In general, facades hide complex class relationships behind a simpler API, whereas wrapper facades hide complex function and data structure API relationships behind richer object-oriented classes. Wrapper facades also provide building-block components that can be ‘plugged’ into higher-level objects or components.

Bridge. The intent of Bridge is to decouple an abstraction from its implementation, so the two can vary independently and dynamically via polymorphism. Wrapper Facade has a similar intent: minimizing the overhead of indirection and polymorphism. Wrapper Facade implementations rarely vary dynamically, however, due to the nature of the systems programming mechanisms that they encapsulate.

Adapter. The intent of Adapter is to convert the interface of a class into another interface that is expected by a client. A common application of Wrapper Facade is to create a set of classes that ‘adapt’ low-level operating system APIs to create a portable set of wrapper facades that appear the same for all applications. Although the structure of this solution is not identical to either the object or class form of Adapter in [GoF95], the wrapper facades play a similar role as an adapter by exporting an object-oriented interface that is common across platforms.

Decorator. The intent of Decorator is to extend an object dynamically by attaching responsibilities transparently. In contrast, Wrapper Facade statically encapsulates lower-level functions and data with object-oriented class interfaces.

In general, Wrapper Facade should be applied in lieu of these other patterns when there are existing lower-level, non-object-oriented APIs to encapsulate, and when it is more important that the solution be efficient than be dynamically extensible.

The Layers pattern [POSA1] helps organize multiple wrapper facades into a separate component layer. This layer resides directly on top of the operating system and shields applications from all the low-level APIs they use.

Credits

Thanks to Brad Appleton, Luciano Gerber, Ralph Johnson, Bob Hanmer, Roger Whitney, and Joe Yoder for extensive comments that improved the form and content of the Wrapper Facade pattern description substantially.

During the public review of Wrapper Facade we debated the best name for this pattern. Several reviewers suggested to call it Wrapper because the Wrapper Facade pattern describes what is often referred to as a ‘wrapper’ by software developers. Unfortunately the term ‘wrapper’ is already overloaded in the patterns community. For example, Wrapper is listed in the Also Known As sections of the Adapter and Decorator patterns [GoF95]. However, the pattern in this book differs from these patterns. We therefore decided to use a non-overloaded name for the pattern we present here.

Component Configurator

The Component Configurator design pattern allows an application to link and unlink its component implementations at run-time without having to modify, recompile, or statically relink the application. Component Configurator further supports the reconfiguration of components into different application processes without having to shut down and re-start running processes.

Also Known As

Service Configurator [JS97b]

Example

A distributed time service [Mil88] [OMG97c] provides accurate clock synchronization for computers that collaborate in local-area or wide-area networks. Its architecture contains three types of components:

  • Time server components answer queries about the current time.
  • Clerk components query one or more time servers to sample their notion of the current time, calculate the ‘approximate’ correct time, and update their own local system time accordingly.
  • Client application components use the globally-consistent time information maintained by their clerks to synchronize their behavior with clients on other hosts.

The conventional way to implement this distributed time service is to configure the functionality of the time server, clerk, and client components statically at compile-time into separate processes running on hosts in the network:

In such a configuration, one or more hosts run processes containing time service components that handle requests for time updates. A clerk component runs in a process on each host on which applications require global time synchronization. Client components in application processes perform computations using the synchronized time reported by their local clerk component.

Although a distributed time service design can be implemented in this way, two general types of problems arise:

  • The choice of component implementation can depend on the environment in which applications run. For example, if a WWV receiver is available,2 the Cristian time service algorithm [Cris89] is most appropriate. Otherwise the Berkeley algorithm [GZ89] is the better choice.

    Changing the environment in which applications run may therefore also require a change to the implementation of time service components. A design in which the implementation of a particular component is fixed statically within a process at compile-time, however, makes it hard to exchange this component’s implementation. In addition, as each component is coupled statically with a process, existing applications must be modified, recompiled, and statically relinked when changes occur.

  • Components may also need to be reconfigured to enhance key quality-of-service (QoS) properties, such as latency and throughput. For example, we can reconfigure the clerk and the time server components in our distributed time service so they are collocated [WSV99] on the same host. In this case, communication overhead can be minimized by allowing the clerk to access the time server’s notion of time via shared memory, rather than exchanging data through a pipe or ‘loopback’ Socket connection.

    However, if components are configured statically into processes, making the changes outlined above requires terminating, reconfiguring, and restarting running time service processes. These activities are not only inefficient, they are potentially infeasible for systems with high availability requirements.

Unfortunately patterns such as Bridge and Strategy [GoF95] are not sufficient by themselves to solve these types of problems. For example, Bridge and Strategy are often used to alleviate unnecessary coupling between components. When these patterns are applied to our example application in isolation, however, all possible implementations of time service components must be configured at compile-time in order to support the selection of different strategies at run-time. This constraint may be excessively inflexible or costly for certain applications.

For example, if a time service runs on a personal computing device with stringent memory and power limitations, components that are not currently in use should be unlinked to minimize resource consumption. This ‘dynamic reconfiguration’ aspect is not addressed directly by patterns such as Bridge and Strategy.

Context

An application or system in which components must be initiated, suspended, resumed, and terminated as flexibly and transparently as possible.

Problem

Applications that are composed of components must provide a mechanism to configure these components into one or more processes. The solution to this problem is influenced by three forces:

  • Changes to component functionality or implementation details are common in many systems and applications. For example, better algorithms or architectures may be discovered as an application matures. It should be possible therefore to modify component implementations at any point during an application’s development and deployment lifecycle.

    Modifications to one component should have minimal impact on the implementation of other components that use it. Similarly, it should be possible to initiate, suspend, resume, terminate, or exchange a component dynamically within an application at run-time. These activities should have minimal impact on other components that are configured into the application.

  • Developers often may not know the most effective way to collocate or distribute multiple component components into processes and hosts at the time an application is developed. If developers commit prematurely to a particular configuration of components it may impede flexibility, reduce overall system performance and functionality, and unnecessarily increase resource utilization.

    In addition, initial component configuration decisions may prove to be sub-optimal over time. For example, platform upgrades or increased workloads may require the redistribution of certain components to other processes and hosts. In such cases, it may be helpful to make these component configuration decisions as late as possible in an application’s development or deployment cycle, without having to modify or shut down an application obtrusively.

  • Performing common administrative tasks such as configuring, initializing, and controlling components should be straightforward and component-independent. These tasks can often be managed most effectively by a central administrator rather than being distributed throughout an application or system. They should be automated whenever possible, for example by using some type of scripting mechanism [MGG00].

Solution

Decouple component interfaces from their implementations and make applications independent of the point(s) in time at which component implementations are configured into application processes.

In detail: a component defines a uniform interface for configuring and controlling a particular type of application service or functionality that it provides. Concrete components implement this interface in an application-specific manner. Applications or administrators can use component interfaces to initiate, suspend, resume, and terminate their concrete components dynamically, as well as to obtain run-time information about each configured concrete component. Concrete components are packaged into a suitable unit of configuration, such as a dynamically linked library (DLL). This DLL can be dynamically linked and unlinked into and out of an application under the control of a component configurator, which uses a component repository to manage all concrete components configured into an application.

Structure

The Component Configurator pattern includes four participants:

A component defines a uniform interface that can be used to configure and control the type of application service or functionality provided by a component implementation. Common control operations include initializing, suspending, resuming, and terminating a component.

Concrete components implement the component control interface to provide a specific type of component. A concrete component also implements methods to provide application-specific functionality, such as processing data exchanged with other connected peer components. Concrete components are packaged in a form that can be dynamically linked and unlinked into or out of an application at run-time, such as a DLL.

Two types of concrete components are used in our distributed time service: time server and clerk. Each of these concrete components provides specific functionality to the distributed time service. The time server component receives and processes requests for time updates from clerks. The clerk component queries one or more time servers to determine the ‘approximate’ correct time and uses this value to update its own local system time. Two time server implementations are available in our example, one for the Cristian algorithm and one for the Berkeley algorithm.

A component repository manages all concrete components that are configured currently into an application. This repository allows system management applications or administrators to control the behavior of configured concrete components via a central administrative mechanism.

A component configurator uses the component repository to coordinate the (re)configuration of concrete components. It implements a mechanism that interprets and executes a script specifying which of the available concrete components to (re)configure into the application via dynamic linking and unlinking from DLLs.

The class diagram for the Component Configurator pattern is as follows:

Dynamics

The behavior of the Component Configurator pattern can be characterized by three phases:

  • Component initialization. The component configurator dynamically links a component into an application and initializes it.3 After a component has been initialized successfully the component configurator adds it to its component repository. This repository manages all configured components at run-time.
  • Component processing. After being configured into an application, a component performs its processing tasks, such as exchanging messages with peer components and performing service requests. The component configurator can suspend and resume existing components temporarily, for example when (re)configuring other components.
  • Component termination. The component configurator shuts down components after they are no longer needed, allowing them the opportunity to clean up their resources before terminating. When terminating a component, the component configurator removes it from the component repository and unlinks it from the application’s address space.

The following state chart diagram illustrates how a component configurator controls the lifecycle of a single concrete component:

This diagram illustrates the event-driven ‘inversion of control’ [Vlis98a] behavior of a component configurator. For example, in response to the occurrence of events like CONFIGURE and TERMINATE, the component configurator invokes the component’s corresponding method, in this case init() and fini(), respectively.

Implementation

The participants in the Component Configurator pattern can be decomposed into two layers:

  • Configuration management layer components. This layer performs general-purpose, application-independent strategies that install, initialize, control, and terminate components.
  • Application layer components. This layer implements the concrete components that perform application-specific processing.

The implementation activities in this section start at the ‘bottom’ with the configuration management layer and work upwards to components in the application layer.

1 Define the component configuration and control interface. Components should support the following operations so that they can be configured and controlled by a component configurator:
  • Component initialization. Initialize or re-initialize a component.
  • Component finalization. Shut down a component and clean up its resources.
  • Component suspension. Suspend component execution temporarily.
  • Component resumption. Resume execution of a suspended component.
  • Component information. Report information describing the static or dynamic directives of a component.

The interface used to configure and control a component can be based on either an inheritance or a message passing strategy:

  • Inheritance-based interface. In this strategy, each component inherits from a common base class that contains pure virtual hook methods [Pree95] for each component configuration and control operation.

The following abstract Component class is based on the ACE framework [SchSu94]:

class Component: public Event_Handler {
public:
   // Initialization and termination hooks.
   virtual void init (int argc, const char *argv[]) = 0;
   virtual void fini () = 0;
 
   // Scheduling hooks.
   virtual void suspend ();
   virtual void resume ();
 
   // Status information hook.
   virtual void info (string &status) const = 0;
};

The component execution mechanism for our time service example is based on a reactive event handling model within a single thread of control, as described by the Reactor pattern (179). By inheriting from the Reactor pattern’s Event_Handler participant, a Component implementation can register itself with a reactor, which then demultiplexes and dispatches events to the component.

  • Message-based interface. Another strategy for configuring and controlling components is to program them to respond to a set of messages, such as INIT, SUSPEND, RESUME, and FINI, sent to the component from the component configurator. Component developers must write code to process these messages, in this case to initialize, suspend, resume, and terminate a component, respectively. Using messages rather than inheritance makes it possible to implement the Component Configurator pattern in non-object-oriented programming languages that lack inheritance, such as C or Ada 83.
2 Implement a component repository. All concrete component implementations that are linked into an application via DLLs are managed by a component repository. A component configurator uses this repository to control a component when it is configured into or out of an application. Each component’s current status, such as whether it is active or suspended, can be maintained in the repository.

A component repository can be a reusable container, for example a Java java.util.Hashtable [Sun00a] or a C++ standard template library map [Aus98]. Conversely it can be implemented as a container in accordance with the Manager pattern [Som97]. This container can be stored in main memory, a file system, or shared memory. Depending on where it resides, a component repository can be managed within the application or by a separate process.

The interface of our Component_Repository class is also based on the ACE framework [SchSu94]:

class Component_Repository {
public:
   // Initialize and close down the repository.
   Component_Repository ();
   ~Component_Repository ();
 
   // Insert a new <Component> with <component_name>.
   void insert (const string &component_name,
              Component *);
 
   // Find <Component> associated with <component_name>.
   Component *find (const string &component_name);
 
   // Remove <Component> associated with
   // <component_name>.
   void remove (const string &component_name);
 
   // Suspend/resume <Component> associated with
   // <component_name>.
   void suspend (const string &component_name);
   void resume (const string &component_name);
private:
   // …
};
3 Implement the component (re)configuration mechanism. A component must be configured into an application’s address space before it can be executed. The component configurator defines a mechanism to control the static and/or dynamic (re)configuration of components into application processes. The implementation of a component configurator involves five sub-activities:
3.1 Define the component configurator interface. The component configurator is often implemented as a singleton facade [GoF95]. This can mediate access to other Component Configurator pattern components, such as the component repository described in implementation activity 2 (84) and the mechanism for interpreting the component configuration directives described in implementation activity 3.3 (88).

The following C++ interface, which is also based on ACE, is the singleton facade used for our distributed time server example:

class Component_Configurator {
public:
   // Initialize the component configurator.
   Component_Configurator ();
 
   // Close down the component configurator and free up
   // dynamically allocated resources.
   ~Component_Configurator ();
 
   // Process the directives specified in the
   // <script_name>.
   void process_directives (const string &script_name);
 
   // Process a single directive specified as a string.
   void process_directive
      (const string &directive_string);
 
   // Accessor to the <Component_Repository>.
   Component_Repository *component_repository ();
 
   // Singleton accessor.
   static Component_Configurator *instance ();
private:
   // …
};
3.2 Define a language for specifying component configuration directives. These directives supply the component configurator with the information it needs to locate and initialize a component’s implementation at run-time, as well as to suspend, resume, re-initialize, and/or terminate a component after it has been initialized. Component configuration directives can be specified in various ways, such as via the command line, environment variables, a graphical user interface, or a configuration script.

To simplify installation and administration, the component configurator in our distributed time server example uses a component scripting mechanism similar to the one provided by ACE [SchSu94]. A script file, which we call comp.conf, consolidates component configuration directives into a single location that can be managed centrally by applications, developers, or administrators. Every component to be (re)configured into an application is specified by a directive in the comp.conf script.

The following comp.conf script illustrates how a time server can be configured dynamically into an application:

# Configure a Time Server.
dynamic Time_Server Component *
   cristian.dll:make_Time_Server()
      “-p $TIME_SERVER_PORT”

The directive in this comp.conf script contains a dynamic command, which instructs the interpreter to perform two actions:

  • Dynamically link the cristian.dll DLL into the application’s address space and
  • Invoke the make_Time_Server() factory function automatically. This function allocates a new time server instance dynamically:
// Keep C++ compiler from non-portably mangling name!
extern “C”
Component *make_Time_Server () {
   // <Time_Server> inherits from the <Component>
   // class.
   return new Time_Server;
}

The string parameter “-p $TIME_SERVER_PORT” at the end of the directive contains an environment variable that specifies the port number on which the time server component listens to receive connections from clerks. The component configurator converts this string into an ‘argc/argv’-style array and passes it to the init() hook method of the time server component. If the init() method initializes the component successfully, a pointer to the component is stored in the component repository under the name ‘Time_Server’. This name identifies the newly-configured component so that it can be controlled dynamically by the component configurator on behalf of an application or an administrator.

The directives in a comp.conf script are processed by the component configurator’s directive interpreter, as described in implementation activity 3.3 (88). Each directive begins with a command that instructs the interpreter how to configure, reconfigure, or control a component:

Command Description
dynamic Dynamically link and initialize a component
static Initialize a statically linked component
remove Remove a component from the component repository and unlink it
suspend Suspend a component temporarily without removing it
resume Resume a previously suspended component

Directives can be written using a simple configuration scripting language defined by the following BNF grammar:

<directive>::= <dynamic> | <static> | <suspend>
                      | <resume> | <remove>
<dynamic> ::= dynamic <comp-location> <parameters-opt>
<static> ::= static <comp-name> <parameters-opt>
<suspend> ::= suspend <comp-name>
<resume> ::= resume <comp-name>
<remove> ::= remove <comp-name>
<comp-location> ::= <comp-name> <type> <function-name>
<type> ::= Component ‘*’ | NULL
<function-name> ::= STRING ‘:’ STRING ’(’’)’
<parameters-opt> ::= ‘“‘ STRING ’“‘| NULL
<comp-name> ::= STRING
3.3 Implement a mechanism for parsing and processing component configuration directives. This mechanism is often implemented as a directive interpreter that decouples the configuration-related aspects of a component from its run-time aspects. A directive interpreter can be implemented using the Interpreter pattern [GoF95], or standard parser-generator tools, such as lex and yacc [SchSu94].

The Component_Configurator facade class defines two methods that allow applications to invoke a component configurator’s directive interpreter. The process_directives() method can process a sequence of (re)configuration and control directives that are stored in a designated script file. This method allows multiple directives to be stored persistently and processed iteratively. Conversely, the process_directive() method can process a single directive passed as a string parameter. This method allows directives to be created dynamically and/or processed interactively.

A simple directive interpreter executes each component configuration directive in the order in which they are specified. In this case, application developers are responsible for ensuring this execution sequence satisfies any ordering dependencies among components being configured. A more complex interpreter and scripting language could of course be devised to allow the directive interpreter to handle ordering dependencies automatically, for example by using topological sorting.

3.4 Implement the dynamic configuration mechanism. A component configurator uses this mechanism to link and unlink components into and out of an application process dynamically. Modern operating systems, such as System V Release 4 (SVR4) UNIX and Win32, support this feature via explicit dynamic linking mechanisms [WHO91].

SVR4 UNIX, for example, defines the dlopen(), dlsym(), and dlclose() API to link a designated DLL dynamically into an application process explicitly, extract a designated factory function from the DLL, and unlink the DLL, respectively. Microsoft’s Win32 operating systems support the LoadLibrary(), GetProcAddr(), and CloseHandle() APIs to perform the same functionality. As the component configurator’s directive interpreter parses and processes directives, it uses these APIs to link and unlink DLLs dynamically into the application’s address space.

Our Component_Configurator implementation uses the following explicit dynamic linking API, based on the wrapper facade (47) defined in ACE [Sch97]:

class DLL {
   // This wrapper facade defines a portable interface to
   // program various DLL operations. The <OS::*>
   // methods are lower-level wrapper facades that
   // encapsulate the variation among explicit dynamic
   // linking APIs defined on different operating
   // systems.
public:
   // Opens and dynamically links the DLL <dll_name>.
   DLL (const string &dll_name) {
      handle_ = OS::dlopen (dll_name.c_str ());
   }
 
   // Unlinks the DLL opened in the constructor.
   ~DLL () { OS::dlclose (handle_); }
 
   // If <symbol_name> is in the symbol table of the DLL
   // return a pointer to it, else return 0.
   void *symbol (const string &symbol_name) {
      return OS::dlsym (handle_, symbol_name.c_str ();
   }
private:
   // Handle to the dynamically linked DLL.
   HANDLE handle_;
};

To illustrate how a component configurator can use this API, consider the directive used to configure a Time_Server component shown in implementation activity 3.2 (86). In this example the component configurator performs seven steps:

1 It creates a DLL object and passes the ‘cristian.dll’ string to its constructor.
2 The cristian.dll DLL is then linked into the application’s address space dynamically via the OS::dlopen() method called in the DLL class constructor.
3 The component configurator next passes the string ‘make_Time_Server()’ to the symbol() method of the DLL object.
4 This method uses the OS::dlsym() method to locate the make_Time_Server entry in the symbol table of the cristian.dll DLL and returns a pointer to this factory function.
5 Assuming the first four steps succeed, the component configurator invokes the factory function, which returns a pointer to a Time_Server component.
6 The component configurator then calls the init() method of this component, passing the string ‘-p $TIME_SERVER_PORT’ as an ‘argc/argv’-style array. The init() method is a hook that the Time_Server component uses to initialize itself.
7 Finally, the component configurator stores the initialized Time_Server component into its component repository.
3.5 Implement the dynamic reconfiguration mechanism. This mechanism builds on the dynamic configuration mechanism described above to trigger dynamic reconfiguration of component implementations. Component reconfiguration should have minimal impact on the execution of other components in an application process. The following two aspects should therefore be addressed when implementing a dynamic reconfiguration mechanism:

Define the reconfiguration triggering strategy. There are two strategies for triggering component reconfiguration, in-band and out-of-band:

  • An in-band strategy initiates reconfigurations synchronously by using an IPC mechanism, such as a Socket connection or a CORBA operation. The application and/or component configurator is responsible for checking for such a reconfiguration event at designated ‘reconfiguration points’.
  • An out-of-band strategy generates an asynchronous event, such as a UNIX SIGHUP signal, that can interrupt a running application process or thread to initiate reconfiguration. In either case, on receiving a reconfiguration event the component configurator will interpret a new set of component configuration directives.

An in-band strategy for triggering reconfiguration is generally easier to implement, because there is less potential for race conditions. In-band triggering may, however, be less responsive, because reconfiguration can only occur at designated reconfiguration points. In contrast, out-of-band reconfiguration triggering is more responsive. However, it is harder to use out-of-band reconfiguration to implement robust protocols for determining when configuration can occur.

Define protocols for ensuring robust reconfiguration. Another important aspect to consider when implementing a reconfiguration mechanism is robustness. For example, if other components in an application are using a component that is being reconfigured, a component configurator may not be able to execute requests to remove or suspend this component immediately. Instead, certain components must be allowed to finish their computation before reconfiguration can be performed.

If a new component is configured into an application, other components may want to be notified, so that they can interact with the new component. Similarly, when a suspended component is resumed, other components may want to be notified so that they can resume their computations.

The Component Configurator pattern focuses on (re)configuration mechanisms, such as how to interpret a script containing component configuration directives to link and unlink components dynamically. It is therefore beyond the scope of Component Configurator to ensure robust dynamic component reconfiguration unilaterally. Supporting robust reconfiguration requires collaboration between a component configurator and component/configuration-specific protocols. These protocols determine when to trigger a reconfiguration and which components to link and interact with to configure particular application processes.

One way to implement a robust reconfiguration mechanism is to apply the Observer pattern [GoF95]. Client components that want to access a particular component are observers. These observers register with the component configurator, which contains a notifier that plays the role of the Observer pattern’s subject participant.

When a component is scheduled for termination, the component configurator implements a two-phase protocol. The first phase notifies its registered client component ‘observers’ to finish their computations. In the second phase, the component configurator removes the component after all client components acknowledge this notification. When a new component is initialized, the component configurator re-notifies its registered client components to indicate that they can connect to the new component.

Similarly, client components can register with the component configurator and be notified when a particular component’s execution is suspended and resumed.

For example, the following changes could be made to the Component and Component_Configurator classes to support the Observer-based reconfiguration mechanism:

class Component : public Event_Handler {
public:
   // Hook method called back when <observed_component>
   // receives a configuration-related event.
   virtual void handle_update
            (Component *observed_component,
             Configuration_Event_Type event);
   // …
};
 
class Component_Configurator {
public:
   // Type of configuration-related events.
   enum Event_Type { INIT, SUSPEND, RESUME, FINI };
 
   // Register <notified_component> to receive
   // notifications when <observed_component> is
   // reconfigured or suspended/resumed.
   void register_observer
         (Component *notified_component,
          Component *observed_component);
   // …
};
4 Implement the concrete components. Concrete component classes can be derived from a common base class such as the Component class specified in implementation activity 1 (82). They can also be implemented via a message-passing mechanism that allows them to receive and process component control messages. Components often implement other methods, such as establishing connections with remote peer components and processing service requests received from clients. Component implementations typically reside in DLLs, though they can also be linked statically with the application.

Implementing concrete components involves three sub-activities:

4.1 Implement the concrete component concurrency model. An important aspect of implementing a concrete component involves selecting the component’s concurrency strategy. For example, a component configured into an application by a component configurator can be executed using event demultiplexing patterns such as Reactor (179) or Proactor (215), or concurrency patterns, such as Active Object (369), Monitor Object (399), Half-Sync/Half-Async (423), or Leader/Followers (447):
  • Reactive/proactive execution. Using these strategies, one thread of control can be used to process all components reactively or proactively. Components implemented using the Reactor pattern are relatively straightforward to (re)configure and control, because race conditions are minimized or eliminated. However, reactive components may not scale as well as other strategies because they are single-threaded.

    Conversely, components using the Proactor pattern may be more efficient than reactive implementations on platforms that support asynchronous I/O efficiently. However, it may be more complicated to reconfigure and control proactive components, due to the subtleties of canceling asynchronous operations. See the Proactor pattern’s liability discussion on page 258 for more details.

  • Multi-threaded or multi-process concurrent execution. Using these strategies, the configured components execute in their own threads or processes after being initialized by a component configurator. For instance, components can run concurrently using the Active Object pattern (369), or execute within a pre-spawned pool of threads or processes in accordance with the Leader/Followers (447) or Half-Sync/Half-Async (423) patterns.

    In general, executing components in one or more threads within the same process as the component configurator may be more efficient than running the components in separate processes. Conversely, configuring components into separate processes may be more robust and secure, because each component can be isolated from accidental corruption via operating system and hardware protection mechanisms [Sch94].

4.2 Implement a mechanism for inter-component communication. Some components run in complete isolation, whereas other components must communicate with one another. In the latter case, component developers must select a mechanism for inter-component communication.

The choice of mechanism is often guided by whether the communicating components will be collocated or distributed:

  • When components are collocated, the choice is typically between hard-coding pointer relationships between components, which is inflexible and can defeat the benefits of dynamic component configuration, versus accessing components ‘by name’ using a component repository.

    Applications in our time service example use a template to retrieve concrete components from a singleton Component_Configurator’s Component_Repository in a type-safe manner:

template <class COMPONENT>
class Concrete_Component {
public:
   // Return a pointer to the <COMPONENT> instance
   // in the singleton <Component_Configurator>’s
   // <Component_Repository> associated with <name>.
   static COMPONENT *instance (const string &name);
};

The instance() method is implemented as follows:

template <class COMPONENT>
COMPONENT *Concrete_Component<COMPONENT>::instance
   (const string &name) {
   // Find the <Component> associated with <name>,
   // and downcast to ensure type-safety.
   Component *comp = Component_Configurator::
      instance ()->component_repository ()->find(name);
   return dynamic_cast<COMPONENT *> (comp);
}

This template is used to retrieve components from the component repository:

Time_Server *time_server =
   Concrete_Component<Time_Server>::instance
      (“Time_Server”);
// Invoke methods on the <time_server> component…
  • When components are distributed, the typical choice is between low-level IPC mechanisms, such as TCP/IP connections programmed using Sockets [Ste98] or TLI [Rago93], and higher-level mechanisms, such as CORBA [OMG98a]. One of the benefits of using CORBA is that the ORB can transparently optimize for the fastest IPC mechanism, by determining automatically whether the component is collocated or distributed [WSV99].
4.3 Implement a mechanism to re-establish component relationships. As outlined in implementation activity 4.2 (93), components can use other components, or even other objects in an application, to perform the services they offer. Replacing one component implementation with another at run-time therefore requires the component configurator to reconnect the new component automatically with components used by the removed component.

One strategy for implementing this mechanism is to checkpoint a component’s references to its related components and store it in a Memento [GoF95]. This memento can be passed to the component configurator before shutting down the component. Similarly, the memento may contain additional state information passed from the old to the new component. After the new component is installed, the component configurator can pass the memento to the new component. This component then re-installs the connections and state information that were saved in the memento.

Implementing a mechanism to save and re-establish component relationships would require three changes to our Component_Configurator and Component classes:

  • Define a Memento hierarchy. For every concrete component type, define a memento that saves the references that the component type can maintain to other components. A reference can be denoted either by a component’s name or by a pointer, as outlined in implementation activity 4.2 (93). All mementos derive from an abstract memento. This allows the Component_Configurator to handle arbitrary mementos using polymorphism.
  • Implement a mechanism for maintaining mementos in the component configurator. During a component’s reconfiguration, the memento containing references to other components is stored in the Component_Configurator. The corresponding infrastructure for handling this memento within the Component_Configurator can contain a reference to the memento, as well as the component type whose references the memento stores.
  • Change the component interface and implementation. To pass a memento from a component to the Component_Configurator and vice versa, we must change the Component interface. For example, the memento can be passed to a Component as a parameter to its init() method, and back to the Component_Configurator via a parameter in the Component’s fini() method. Within the init() and fini() method implementations of concrete components, the memento is then used to retrieve and save the component’s relationships to other components and objects.

In addition to component references, the memento could maintain other state information that is passed to the new component. For example, Clerk components could pass the frequency at which they poll time servers, so that new Clerk components can update their local system time at the same frequency.

In the remainder of this section, we show how the implementation activity 4 (92) and its sub-activities can be applied to guide the implementation of concrete component participants in our distributed time service example.

There are two types of concrete components in a distributed time service: Time_Server and Clerk. The Time_Server component receives and processes requests for time updates from Clerks. Both Time_Server and Clerk components are designed using the Acceptor-Connector pattern (285). As outlined in implementation activity 1 (82), the component execution mechanism for the Time_Server and Clerk is based on a reactive event-handling model within a single thread of control, in accordance with the Reactor pattern (179).

The Time_Server inherits from the Component class:

class Time_Server : public Component {
public:
   // Initialize and terminate a <Time_Server>.
   virtual void init (int argc, const char *argv[]);
   virtual void fini ();
 
   // Other methods (e.g., <info>, <suspend>, and
   // <resume>) omitted.
private:
   // The <Time_Server_Acceptor> that creates, accepts,
   // and initializes <Time_Server_Handler>s.
   Time_Server_Acceptor acceptor_;
 
   // A C++ standard library <list> of
   // <Time_Server_Handler>s.
   list<Time_Server_Handler *> handler_list_;
};

By inheriting from Component, Time_Server objects can be linked and unlinked by the Component_Configurator dynamically. This design decouples the implementation of the Time_Server from the time or context when it is configured, allowing developers to switch readily between different Time_Server algorithms.

Before storing the Time_Server component in its component repository, the application’s component configurator singleton invokes the component’s init() hook method. This allows the Time_Server component to initialize itself.

Internally, the Time_Server contains a Time_Server_Acceptor that listens for connection requests to arrive from Clerks. It also contains a C++ standard template library [Aus98] list of Time_Server_Handlers that process time update requests. The Time_Server_Acceptor is created and registered with a reactor when the Time_Server’s init() method is called.

When a new connection request arrives from a Clerk, the acceptor creates a new Time_Server_Handler, which processes subsequent time update requests from the Clerk. When its init() method is invoked by the Time_Server_Acceptor, each handler registers itself with the singleton reactor, which subsequently dispatches the handler’s handle_event() method when time update requests arrive.

When a component configurator terminates a Time_Server, it calls the Time_Server’s fini() method. This method unregisters the Time_Server_Acceptor and all of its associated Time_Server_Handlers from the reactor and destroys them.

We provide two Time_Server component implementations:

  • The first component implements Cristian’s algorithm [Cris89]. In this algorithm each Time_Server is a passive entity that responds to queries made by Clerks. In particular, a Time_Server does not query other machines actively to determine its own notion of time.
  • The second component implements the Berkeley algorithm [GZ89]. In this algorithm, the Time_Server is an active component that polls every machine in the network periodically to determine its local time. Based on the responses it receives, the Time_Server computes an aggregate notion of the correct time.

As with the Time_Server above, the Clerk inherits from the Component class:

class Clerk : public Component {
public:
   // Initialize and terminate a <Clerk>.
   virtual void init (int argc, const char *argv[]);
   virtual void fini ();
 
   // <info>, <suspend>, and <resume> methods omitted.
 
   // Hook method invoked by a <Reactor> when a timeout
   // occurs periodically. This method contacts several
   // <Time_Server>s to compute its local notion of time.
   virtual void handle_event (HANDLE, Event_Type);
private:
   // The <Clerk_Connector> that connects and
   // initializes <Clerk_Handler>s.
   Clerk_Connector connector_;
 
   // A C++ standard library <list> of <Clerk_Handler>s.
   list<Clerk_Handler *> handler_list_;
};

By inheriting from Component, the Clerk can be linked and unlinked dynamically by a component configurator. Similarly, a component configurator can configure, control and reconfigure the Clerk it manages by calling its init(), suspend(), resume(), and fini() hook methods.

Our Clerk component establishes and maintains connections with Time_Servers and queries them to calculate the current time. The Clerk’s init() method dynamically allocates Clerk_Handlers that send time update requests to Time_Servers connected via a Clerk_Connector. It also registers the Clerk with a reactor to receive timeout events periodically, such as every five minutes.

When the timeout period elapses, the reactor notifies the Clerk’s handle_event() hook method. This method instructs the Clerk’s Clerk_Handlers to request the current time at the time servers to which they are connected. The Clerk receives and processes these server replies, then updates its local system time accordingly. When Clients ask the Clerk component for the current time, they receive a locally-cached time value that has been synchronized with the global notion of time. The Clerk’s fini() method shuts down and cleans up its connector and handlers.

The two alternative implementations of the time services are provided within two DLLs. The cristian.dll contains a factory that creates components that run the Cristian algorithm. Likewise, the berkeley.dll contains a factory that creates components that run the Berkeley algorithm.

Example Resolved

In this section, we show how our example distributed time server implementation applies the Component Configurator pattern using a configuration mechanism based on explicit dynamic linking [SchSu94] and a comp.conf configuration script. The example is presented as follows:

  • We first show how the configuration mechanism supports the dynamic configuration of Clerk and Time_Server components into application processes via scripting.
  • We then show how these features allow Clerk components to change the algorithms used to compute local system time. In particular, after a new algorithm has been selected, a singleton Component_Configurator can reconfigure the Clerk component dynamically without affecting the execution of other types of components controlled by the component configurator.

There are two general strategies for configuring a distributed time component application: collocated and distributed. We outline each strategy to illustrate how a component configurator-enabled application can be dynamically (re)configured and run.

Collocated configuration. This configuration uses a comp.conf script to collocate the Time_Server and the Clerk within the same process. A generic main() program configures components dynamically using the process_directives() method of the Component_Configurator object and then runs the application’s event loop. This event loop is based on the Reactor pattern (179):

int main (int argc, char *argv[]) {
   Component_Configurator server;
 
   // Interpret the comp.conf file specified in argv[1].
   server.process_directives (argv[1]);
   // Reactor singleton perform component processing and
   // any reconfiguration updates.
   for (;;)
      Reactor::instance ()->handle_events ();
   /* NOTREACHED */
}

The process_directives() method configures components into the server process dynamically as it interprets the following comp.conf configuration script:

# Configure a Time Server.
dynamic Time_Server Component *
   cristian.dll:make_Time_Server()
      “-p $TIME_SERVER_PORT”
 
# Configure a Clerk.
dynamic Clerk Component *
   cristian.dll:make_Clerk()
      “-h tango.cs:$TIME_SERVER_PORT”
      “-h perdita.wuerl:$TIME_SERVER_PORT”
      “-h atomic-clock.lanl.gov:$TIME_SERVER_PORT”
      “-P 10” # polling frequency

The directives in comp.conf specify to the Component_Configurator how to configure a collocated Time_Server and Clerk dynamically in the same application process using the Cristian algorithm. The Component_Configurator links the cristian.dll DLL into the application’s address space dynamically and invokes the appropriate factory function to create new component instances. In our example, these factory functions are called make_Time_Server() and make_Clerk(), which are defined as follows:

Component *make_Time_Server () { return new Time_Server; }
Component *make_Clerk () { return new Clerk; }

After each factory function returns its new allocated component, the designated initialization parameters in the comp.conf script are passed to the respective init() hook methods. These perform the corresponding component-specific initialization, as illustrated in implementation activity 4 (92).

Distributed configuration. To reduce the memory footprint of an application, we may want to collocate the Time_Server and the Clerk in different processes. Due to the flexibility of the Component Configurator pattern, all that is required to distribute these components is to split the comp.conf script into two parts and run them in separate processes or hosts. One process contains the Time_ Server component and the other process contains the Clerk component.

The figure below shows what the configuration looks like with the Time_Server and Clerk collocated in the same process, as well as the new configuration after the reconfiguration split. Note that the components themselves need not change, because the Component Configurator pattern decouples their processing behavior from the point in time when they are configured.

Reconfiguring an application’s components. Now consider what happens if we decide to change the algorithms that implement components in the distributed time service. For example, we may need to switch from Cristian’s algorithm to the Berkeley algorithm to take advantage of new features in the environment. For example, if the machine on which the Time_Server resides has a WWV receiver, the Time_Server can act as a passive entity and the Cristian algorithm may be appropriate. Conversely, if the machine on which the Time_Server resides does not have a WWV receiver, an implementation of the Berkeley algorithm may be more appropriate.

Ideally, we should be able to change Time_Server algorithm implementations without affecting the execution of other components of the distributed time service. Accomplishing this using the Component Configurator pattern simply requires minor modifications to our distributed time service configuration activities:

1 Modify the existing comp.conf script. We start by making the following change to the comp.conf script:
# Shut down <Time_Server>.
remove Time_Server

This directive instructs the Component_Configurator to shut down the Time_Server component, remove it from the Component_Repository, and unlink the cristian.dll if there are no more references to it.

2 Notify the component configurator to reinterpret the comp.conf script. Next we must instruct the Component_Configurator to process the updated comp.conf script. This can be triggered either in-band, such as via a Socket connection or a CORBA operation, or out-of-band, such as via a UNIX SIGHUP signal. Regardless of which triggering strategy is used, after the Component_Configurator receives a reconfiguration event, it consults its comp.conf script again and shuts down the Time_Server component by calling its fini() method. During this step the execution of other components should be unaffected.
3 Initiate reconfiguration. We can now repeat steps 1 and 2 to reconfigure the Berkeley Time_Server component implementation into an application. The comp.conf script must be modified with a new directive to specify that the Berkeley Time_Server component be linked dynamically from the berkeley.dll DLL:
# Configure a Time Server.
dynamic Time_Server Component *
   berkeley.dll:make_Time_Server()
      “-p $TIME_SERVER_PORT”

Finally, an event is generated to trigger the Component_Configurator in the process to reread its comp.conf script and add the updated Time_Server component to the Component_Repository. This component starts executing immediately after its init() method is invoked successfully.

The ease with which new component implementations can be replaced dynamically exemplifies the flexibility and extensibility provided by the Component Configurator pattern. In particular, no other configured components in an application should be affected when the Component_Configurator removes or reconfigures the Time_Server component.

Known Uses

The Windows NT Service Control Manager (SCM). The SCM allows a master SCM process to initiate and control administrator-installed service components automatically using the message-based strategy described in the Implementation section. The master SCM process initiates and manages system service components by passing them various control messages, such as PAUSE, RESUME, and TERMINATE, that must be handled by each service component. SCM-based service components run as separate threads within either a single-service or a multi-service server process. Each installed service component is responsible for configuring itself and monitoring any communication endpoints, which can be more general than socket ports. For instance, the SCM can control named pipes and shared memory.

Modern operating system device drivers. Most modern operating systems, such as Solaris, Linux, and Windows NT, provide support for dynamically-configured kernel-level device drivers. These drivers can be linked into and unlinked out of the system dynamically via hooks, such as the init(), fini(), and info() functions defined in SVR4 UNIX [Rago93]. These operating systems apply the Component Configurator pattern to allow administrators to reconfigure the operating system kernel without having to shut it down, recompile, and statically relink new drivers and restart it.

Java applets. The applet mechanism in Java supports dynamic downloading, initializing, starting, stopping, and terminating of Java applets. Web browsers implement the infrastructure software to actually download applets and prepare them for execution. The class java.applet.Applet provides empty methods init(), start(), stop(), and destroy(), to be overridden in application-specific subclasses. Java therefore uses the inheritance-based strategy described in the Implementation section. The four life-cycle hook methods mentioned above are called by the browser at the correct time. They give the applet a chance to provide custom behavior that will be called at appropriate times.

For example, the init() hook will be called by the browser once the applet is loaded. The start() hook will be called once set-up is complete and the applet should start its application logic. The stop() hook will be called when the user leaves the Web site. Note that start() and stop() can be called repeatedly, for example when the user visits and leaves a Web site multiple times. The destroy() hook is called once the applet is reclaimed and should free all resources. Finer-grained life-cycle behavior inside an applet can be achieved by creating multiple threads inside the applet and having them scheduled as in ordinary Java applications. Additional examples of how the Component Configurator pattern is used for Java applets are presented in [JS97b].

The dynamicTAO reflective ORB [KRL+00] implements a collection of component configurators that allow the transfer of components across a distributed system, loading and unloading modules into the ORB run-time system, and inspecting and modifying the ORB configuration state. Each component configurator is responsible for handling the (re)configuration of a particular aspect of dynamicTAO. For example, its TAOConfigurator component configurator contains hooks to which implementations of concurrency and scheduling strategies, as well as security and monitoring interceptors (109), can be attached. In addition, a DomainConfigurator provides common services for loading and unloading components into dynamicTAO. It is the base class from which all other component configurators derive, such as TAOConfigurator.

ACE [Sch97]. The ADAPTIVE Communication Environment (ACE) framework provides a set of C++ mechanisms for configuring and controlling components dynamically using the inheritance-based strategy described in the Implementation section. The ACE Service Configurator framework [SchSu94] extends the mechanisms provided by Inetd, Listen, and SCM to support automatic dynamic linking and unlinking of communication service components.

The Service Configurator framework provided by ACE was influenced by the mechanisms and patterns used to configure and control device drivers in modern operating systems. Rather than targeting kernel-level device drivers, however, ACE focuses on dynamic configuration and control of application-level components. These ACE components are often used in conjunction with the Reactor (179), Acceptor-Connector (285), and Active Object (369) patterns to implement communication services.

In football, which Americans call soccer, each team’s coach can substitute a limited number of players during a match. The coach is the component configurator who decides which players to substitute, and the players embody the role of components. All players obey the same protocol with respect to substitution, which occurs dynamically, that is, the game does not stop during the substitutions. When players see a sign waved with their numbers, they leave the field and new players join the game immediately. The coach’s list of the current 11 players corresponds to the Component Repository. Just as the reconfiguration script is not always written by the coach: some home crowds are renowned for asking and shouting for specific players to be put into the game—and for firing the coach.

Consequences

The Component Configurator pattern offers the following benefits:

Uniformity. The Component Configurator pattern imposes a uniform configuration and control interface for managing components. This uniformity allows components to be treated as building blocks that can be integrated as components into a larger application. Enforcing a common interface across all components makes them ‘look and feel’ the same with respect to their configuration activities, which simplifies application development by promoting the ‘principle of least surprise’.

Centralized administration. The Component Configurator pattern groups one or more components into a single administrative unit. This consolidation simplifies development by enabling common component initialization and termination activities, such as opening/closing files and acquiring/releasing locks, to be performed automatically. In addition, the pattern centralizes the administration of components by ensuring that each component supports the same configuration management operations, such as init(), suspend(), resume(), and fini().

Modularity, testability, and reusability. The Component Configurator pattern improves application modularity and reusability by decoupling the implementation of components from the manner in which the components are configured into processes. Because all components have a uniform configuration and control interface, monolithic applications can be decomposed more easily into reusable components that can be developed and tested independently. This separation of concerns encourages greater reuse and simplifies development of subsequent components.

Configuration dynamism and control. The Component Configurator pattern enables a component to be dynamically reconfigured without modifying, recompiling, or statically relinking existing code. In addition, (re)configuration of a component can often be performed without restarting the component or other active components with which it is collocated. These features help create an infrastructure for application-defined component configuration frameworks.

Tuning and optimization. The Component Configurator pattern increases the range of component configuration alternatives available to developers by decoupling component functionality from component execution mechanisms. For instance, developers can tune server concurrency strategies adaptively to match client demands and available operating system processing resources. Common execution alternatives include spawning a thread or process upon the arrival of a client request or pre-spawning a thread or process at component creation time.

The Component Configurator pattern has several liabilities:

Lack of determinism and ordering dependencies. The Component Configurator pattern makes it hard to determine or analyze the behavior of an application until its components are configured at run-time. This can be problematic for certain types of system, particularly real-time systems, because a dynamically-configured component may not behave predictably when run with certain other components. For example, a newly configured component may consume excessive CPU cycles, thereby starving other components of processing time and causing them to miss deadlines.

Reduced security or reliability. An application that uses the Component Configurator pattern may be less secure or reliable than an equivalent statically-configured application. It may be less secure because impostors can masquerade as components in DLLs. It may be less reliable because a particular component configuration may adversely affect component execution. A faulty component may crash, for example, corrupting state information it shares with other components configured into the same process.

Increased run-time overhead and infrastructure complexity. The Component Configurator pattern adds levels of abstraction and indirection when executing components. For example, the component configurator first initializes components and then links them into the component repository, which may incur excessive overhead in time-critical applications. In addition, when dynamic linking is used to implement components many compilers add extra levels of indirection to invoke methods and access global variables [GLDW87].

Overly narrow common interfaces. The initialization or termination of a component may be too complicated or too tightly coupled with its context to be performed in a uniform manner via common component control interfaces, such as init() and fini().

See Also

The intent of the Component Configurator pattern is similar to the Configuration pattern [CMP95]. The Configuration pattern decouples structural issues related to configuring protocols and services in distributed applications from the execution of the protocols and services themselves. The Configuration pattern has been used in frameworks that support the construction of distributed systems out of building-block components.

In a similar way, the Component Configurator pattern decouples component initialization from component processing. The primary difference is that the Configuration pattern focuses on the active composition of chains of related protocols and services. In contrast, the Component Configurator pattern focuses on the dynamic initialization of components that process requests exchanged between transport endpoints.

Credit

Thanks to Giorgio Angiolini, who provided us with feedback on an earlier version of this pattern. In addition, thanks to Prashant Jain, who was the co-author of the original version of the Service Configurator pattern, which formed the basis for the Component Configurator pattern described here. Fabio Kon contributed the description of the dynamicTAO known use.

Interceptor

The Interceptor architectural pattern allows services to be added transparently to a framework and triggered automatically when certain events occur.

Example

MiddleSoft Inc. is developing an object request broker (ORB) middleware framework called MiddleORB, which is an implementation of the Broker pattern [POSA1]. MiddleORB provides communication services that simplify the development of distributed applications. In addition to core communication services, such as connection management and transport protocols, applications using MiddleORB may require other services, such as transactions and security, load balancing and fault tolerance, auditing, and logging, non-standard communication mechanisms like shared memory, and monitoring and debugging tools.

To satisfy a wide-range of application demands, the MiddleORB architecture must support the integration of these extended services. One strategy for coping [Cope98] with this requirement is to integrate as many services as possible into the default MiddleORB configuration. This strategy is often infeasible, however, because not all ORB services can be anticipated at its development time. As distributed applications evolved, the ORB framework would inevitably expand to include new features. Such piecemeal growth can complicate ORB design and maintenance, as well as increase its memory footprint, even though many of these features are not used by all applications all the time.

An alternative strategy is to keep the MiddleORB framework as simple and concise as possible. In this model, if application developers require services not available in the framework, they would implement them along with their own client and server code. However, this strategy would require developers to implement much code that was unrelated to their application logic.

In addition, certain services cannot be implemented solely at the application client and object level, because they must interact intimately with core ORB features. For example, a security service should be integrated with the ORB infrastructure. Otherwise, applications can masquerade as privileged users and gain unauthorized access to protected system resources.

Clearly, neither strategy outlined above is entirely satisfactory. With the first strategy MiddleORB will be too large and inflexible, whereas with the second, applications will become overly complex and potentially insecure or error-prone. We must therefore devise a better strategy for integrating application-specific services into MiddleORB.

Context

Developing frameworks that can be extended transparently.

Problem

Frameworks, such as ORBs, application servers, and domain-specific software architectures [SG96], cannot anticipate all the services they must offer to users. It may also not be feasible to extend certain types of frameworks, particularly black-box frameworks [HJE95], with new services that they were not originally designed to support. Similarly, it is often undesirable to rely upon applications to implement all the necessary services themselves, because this defeats many benefits of reuse. Framework developers must therefore address the following three forces:

  • A framework should allow integration of additional services without requiring modifications to its core architecture.

    For example, it should be possible to extend MiddleORB to support security services, such as Kerberos or SSL [OSSL00], without modifying the structure of its internal design [OMG98d].

  • The integration of application-specific services into a framework should not affect existing framework components, nor should it require changes to the design or implementation of existing applications that use the framework.

    For instance, adding load balancing to MiddleORB should be unobtrusive to existing MiddleORB client and server applications.

  • Applications using a framework may need to monitor and control its behavior.

    For example, some applications may want to control MiddleORB’s fault tolerance strategies [OMG99g] via the Reflection pattern [POSA1] to direct its responses to failure conditions.

Solution

Allow applications to extend a framework transparently by registering ‘out-of-band’ services with the framework via predefined interfaces, then let the framework trigger these services automatically when certain events occur.4 In addition, open the framework’s implementation [Kic92] so that the out-of-band services can access and control certain aspects of the framework’s behavior.

In detail: for a designated set of events processed by a framework, specify and expose an interceptor callback interface. Applications can derive concrete interceptors from this interface to implement out-of-band services that process occurrences of these events in an application-specific manner. Provide a dispatcher for each interceptor that allows applications to register their concrete interceptors with the framework. When the designated events occur, the framework notifies the appropriate dispatchers to invoke the callbacks of the registered concrete interceptors.

Define context objects to allow a concrete interceptor to introspect and control certain aspects of the framework’s internal state and behavior in response to events. Context objects provide methods to access and modify a framework’s internal state, thus opening its implementation. Context objects can be passed to concrete interceptors when they are dispatched by the framework.

Structure

A concrete framework instantiates a generic and extensible architecture to define the services provided by a particular system, such as an ORB, a Web server, or an application server.

Two types of concrete frameworks are available in MiddleORB, one for the client and one for the server:5

  • Client applications use the client concrete ORB framework’s programming interface to access remote objects. This concrete framework provides common services, such as binding to a remote object, sending requests to the object, waiting for replies, and returning them to the client.
  • The server concrete ORB framework provides complementary services, including registering and managing object implementations, listening on transport endpoints, receiving requests, dispatching these requests to object implementations, and returning replies to clients.

Interceptors are associated with a particular event or set of events exposed by a concrete framework. An interceptor defines the signatures of hook methods [Pree95] [GHJV95] that the concrete framework will invoke automatically via a designated dispatching mechanism when the corresponding events occur. Concrete interceptors specialize interceptor interfaces and implement their hook methods to handle these events in an application-specific manner.

In our MiddleORB example, we specify an interceptor interface containing several hook methods that the client and server concrete ORB frameworks dispatch automatically when a client application invokes a remote operation and the corresponding server receives the new request, respectively.

To allow interceptors to handle the occurrence of particular events, a concrete framework defines dispatchers for configuring and triggering concrete interceptors. Typically there is a dispatcher for each interceptor. A dispatcher defines registration and removal methods that applications use to subscribe and un-subscribe concrete interceptors with the concrete framework.

A dispatcher also defines another interface that the concrete framework calls when specific events occur for which concrete interceptors have registered. When the concrete framework notifies a dispatcher that such an event has occurred, the dispatcher invokes all the concrete interceptor callbacks that have registered for it. A dispatcher maintains all its registered interceptors in a container.

In our MiddleORB example, the client concrete ORB framework implements a dispatcher that allows client applications to intercept certain events, such as outgoing requests to remote objects and incoming object replies. Servers use a corresponding dispatcher in the server concrete ORB framework to intercept related events, such as incoming client requests and outgoing object replies. Other dispatchers can be defined at different layers in the ORB to intercept other types of events such as connection and message transport events.

Concrete interceptors can use context objects to access and control certain aspects of a concrete framework. Context objects can provide accessor methods to obtain information from the concrete framework and mutator methods to control the behavior of the concrete framework. A context object can be instantiated by a concrete framework and passed to a concrete interceptor with each callback invocation. In this case the context object can contain information related to the event that triggered its creation.

Conversely, a context object can be passed to an interceptor when it registers with a dispatcher. This design provides less information but also incurs less overhead.

In our MiddleORB example, the interceptor interface defines methods that the client concrete ORB framework dispatches automatically when it processes an outgoing request. These methods are passed a context object parameter containing information about the current request. Each context object defines accessor and mutator methods that allow a concrete interceptor to query and change ORB state and behavior, respectively.

For example, an accessor method in a context object can return the arguments for a remote operation. Using the context object’s mutator methods, a client application’s concrete interceptor can redirect an operation to a different object. This feature can be used to implement custom load balancing and fault tolerance services [ZBS97].

An application runs on top of a concrete framework and reuses the services it provides. An application can also implement concrete interceptors and register them with the concrete framework to handle certain events. When these events occur, they trigger the concrete framework and its dispatchers to invoke concrete interceptor callbacks that perform application-specific event processing.

The class diagram below illustrates the structure of participants in the Interceptor pattern.

Dynamics

A typical scenario for the Interceptor pattern illustrates how an application implements a concrete interceptor and registers it with the corresponding dispatcher. The dispatcher then invokes the interceptor callback when the concrete framework notifies it that an event of interest has occurred:

  • An application instantiates a concrete interceptor that implements a specific interceptor interface. The application registers this concrete interceptor with the appropriate dispatcher.
  • The concrete framework subsequently receives an event that is subject to interception. In this scenario a special context object is available for each kind of event. The concrete framework therefore instantiates an event-specific context object that contains information related to the event, as well as functionality to access and potentially control the concrete framework.
  • The concrete framework notifies the appropriate dispatcher about the occurrence of the event, passing the context object as a parameter.
  • The dispatcher iterates through its container of registered concrete interceptors and invokes their callback hook methods, passing the context object as an argument.
  • Each concrete interceptor can use its context object to retrieve information about the event or the concrete framework. After processing this information, a concrete interceptor can optionally call method(s) on the context object to control the behavior of the concrete framework and its subsequent event processing.
  • After all concrete interceptor callback methods have returned, the concrete framework continues with its normal operation.

Implementation

Seven implementation activities describe a common approach for implementing the Interceptor pattern.

1 Model the internal behavior of the concrete framework using a state machine or an equivalent notation, if such a model is not available already. This modeling need not capture all abstractions of the concrete framework, but should document the aspects that are related to interception. To minimize the complexity of any given state machine, the modeled parts of the concrete framework can be composed from smaller state machines that together form a composite state machine.6

Each smaller state machine represents a particular aspect of the concrete framework. Once the dynamic aspects of the concrete framework are modeled as a state machine, use this model to determine where and when certain events can be intercepted.

In ORB middleware and many other component-based systems at least two types of concrete frameworks exist, one for the role of client and one for the role of server. In this case the concrete frameworks should be modeled as separate state machines. In general, state machine modeling helps identify where to place interceptors and how to define their behavior in a concrete framework.

Consider the client concrete ORB framework defined by MiddleORB. During ORB start-up this framework is initialized to continue processing client requests until it is shut down. The client concrete ORB framework provides two types of service to clients:

  • When a client binds to a new remote object, the concrete framework creates a proxy that connects to the object.
  • If the bind operation is successful the client can send requests to the remote object. Each request is marshaled and delivered to the remote object using a pre-established connection. After successful delivery, the concrete framework waits for the object’s response message, demarshals it upon arrival, returns the result to the client, and transitions to the idle state.

Additional error states denote situations in which problems are encountered, such as communication errors or marshaling errors, are shown in the following figure. Note that this figure illustrates only a portion of the client concrete ORB framework’s internal composite state machine.

2 Identify and model interception points. This implementation activity can be divided into four sub-activities:
2.1 Identify concrete framework state transitions that may not be visible to external applications, but are subject to interception. For example, a client may want to intercept outgoing requests so it can add functionality, such as logging or changing certain request parameters, dynamically. We call these state transitions ‘interception points’.
2.2 Partition interception points into reader and writer sets. The reader set includes all state transitions in which applications only access information from the concrete framework. Conversely the writer set includes all state transitions in which applications can modify the behavior of the concrete framework.
2.3 Integrate interception points into the state machine model. Interception points can be modeled in the state machine by introducing intermediary states. If a state transition is subject to interception, place a new interception state between the source state and the sink state of the original transition. This interception state triggers the corresponding interceptors. For interception points that belong to the writer set, introduce additional state transitions in which the following properties apply:
  • The interception state is the start node and
  • The target nodes are states that represent the subsequent behavior of the concrete framework after the interception.

Many component-based distributed systems define peer concrete frameworks, such as client and server ORBs, that are organized in accordance with the Layers pattern [POSA1]. When identifying interception points in one of these concrete frameworks, introduce a related interception point in the other peer concrete framework at the same logical layer. For example, if a client ORB intercepts outgoing requests, it is likely that the server ORB should also intercept incoming requests. When integrating layered services, such as adding security tokens on the client-side and encrypting outgoing request data, a corresponding interceptor is therefore required on the server to extract the security token and decrypt the incoming data.

By applying the state machine model of the client concrete ORB framework shown above, we can identify the potential interception points shown in the following table:

Interception Point Description Reader / Writer
Shut-down The concrete framework is shutting down its operation. Clients may need to perform certain cleanup work, such as freeing resources they have allocated previously. Reader
Binding The client application is binding to a remote object. The concrete framework instantiates a new proxy and establishes a communication channel. A monitoring service might intercept this event to visualize new client/object relationships. Reader
PreMarshalOutRequest The client application sends a request to the remote object. Interceptors might be used to change the target object or the parameter values to support load balancing, validate certain preconditions, or encrypt parameters. Reader + Writer
PostMarshalOutRequest The client concrete ORB framework has marshaled the data but not yet delivered it. A client may be interested in monitoring activities, such as starting a timer to measure the round-trip latency. Reader
PreMarshalInReply The reply just arrived and the concrete framework has not yet demarshaled the data. A client may be interested in monitoring this event or stopping a round-trip latency timer. Reader
PostMarshalInReply The client concrete ORB framework has marshaled the reply. An interceptor might evaluate post-conditions or change the result. For example, it could decrypt the result if it was encrypted by a server-side interceptor. Reader + Writer

Additional interception points may be required if a client intercepts exceptions, such as failed connection events. The server concrete ORB framework can also define peer interception points.

2.4 Partition interception points into disjoint interception groups. To process events, concrete frameworks often perform a series of related activities, each of which may be associated with an interception point. To emphasize the relationship between each activity, it may be useful to coalesce a series of semantically-related interception points into an interception group.

For example, all interception points associated with sending a request can form one interception group, whereas all interception points associated with receiving a request can form another group. These interception groups help to minimize the number of necessary interceptors and dispatchers as shown in implementation activity 4 (123).

To identify interception groups, analyze the state machine for interception points that are located in the same area of the state machine and participate in the same activity. For example, interception points that are triggered by transitions originating from a particular state, ending in a particular state, or ending in a particular set of neighbor states may be candidates for consideration as part of the same interception group.

In MiddleORB, both the PreMarshalOutRequest and PostMarshalOutRequest interception points participate in sending a request. These interception points can therefore constitute the OutRequest interception group. This interception group coalesces all events related to the activities of sending a request in order to differentiate these events from other interception groups, such as InRequest, OutReply, or InReply.

3 Specify the context objects. Context objects allow interceptors to access and control aspects of the framework’s internal state and behavior in response to certain events. Three sub-activities can be applied to specify context objects:
3.1 Determine the context object semantics. Context objects provide information about an interception point and may also define services to control the framework’s subsequent behavior. Concrete interceptors use the information and services to handle interception points in an application-specific manner. The accessor and mutator methods defined for context objects can be based on information that a concrete framework provides to interceptors, as well as the degree to which a framework is ‘open’:
  • If an interception point belongs to the reader set, determine what information the concrete framework should provide the interceptor for each event it handles. For example, if a context object provides information about a particular remote operation invocation, it may contain the reference of the target object being called as well as the operation’s name and parameter values.
  • If the interception point belongs to the writer set, determine how to ‘open’ the concrete framework’s implementation so that concrete interceptors can control selected aspects of its behavior [Kic92]. For example, if a context object provides information about a particular remote operation invocation, it may contain methods that can modify the operation’s parameter values. The design force to balance here, of course, is ‘open extensibility’ versus ‘error-prone interception code’.

    Although concrete frameworks with open implementations can have powerful interceptors, they are also more vulnerable to interceptors that maliciously or accidentally corrupt the concrete framework’s robustness and security. Some interception designs therefore disallow mutator functionality within context objects.

3.2 Determine the number of context object types. Here are two strategies for selecting the number and types of context objects:
  • Multiple interfaces. If the interception points in a concrete framework cover a diverse set of requirements, different types of context objects can be defined for different interception points. This strategy is flexible, because it allows fine-grained control of particular interception points. However it increases the number of interfaces that developers of concrete interceptors must understand.
  • Single interface. It is possible to specify a generic context object with a single interface. Using a single interface reduces the number of context object interfaces, but may yield a bloated and complex context object interface.

In general, multiple interfaces are useful when client applications intercept a wide variety of different framework events. In other cases, however, the single interface strategy may be preferable due to its simplicity.

When the MiddleORB client ORB framework intercepts outgoing client requests, applications may want to access and/or control the following aspects:

  • Reading and changing the target object reference to implement fault tolerance or load balancing.
  • Reading and modifying parameter values to encrypt data, validate selected arguments, or change behavior reflectively [POSA1].
  • Adding new data to the request to send out-of-band information, such as security tokens or transaction contexts.
  • Integrating custom parameter marshalers and demarshalers.

These activities correspond to those specified by the PreMarshalOutRequest and PostMarshalOutRequest interception points outlined in the table in implementation activity 2.3 (118). We therefore introduce two corresponding context object types, UnmarshaledRequest and MarshaledRequest. The interface UnmarshaledRequest is structured as follows:

public interface UnmarshaledRequest {
   public String getHost (); // get host
   public void setHost (String host); // set host
   public long getPort (); // get server port
   public void setPort (long newPort); // set new port
   public String getObjName (); // get object name
   public void setObjName (String newName); // set name
   public String getMethod (); // get method name
   public void setMethod (String name); // set method
   public Enumeration getParameters ();// get parameters
   public Object getArg (long i); // get i_th arg
   public void setArg (long i, Object o); // set i_th arg
   public void addInfo (Object info); // add extra info.
   // …
}
3.3 Define how to pass context objects to concrete interceptors. Context objects are instantiated by the concrete framework. They are passed to a concrete interceptor using one of the following two strategies:
  • Per-registration. In this strategy a context object is passed to an interceptor once when it registers with a dispatcher.
  • Per-event. In this strategy a context object is passed to a concrete interceptor with every callback invocation.

The per-event strategy allows a concrete framework to provide fine-grained information about the occurrence of a particular event. In contrast, the per-registration strategy only provides general information common to all occurrences of a particular event type. The per-event strategy may incur higher overhead, however, due to repeated creation and deletion of context objects.

4 Specify the interceptors. An interceptor defines a generic interface that a concrete framework uses to invoke concrete interceptors, via dispatchers, when interception points are triggered. An interceptor is defined for each interception group identified in implementation activity 2.4 (120). Consequently each concrete interceptor that derives from a particular interceptor is responsible for handling all the interception points of a specific interception group.

For each interception point in an interception group, an interceptor defines a designated callback hook method. There is thus a one-to-one relationship between an interception point and an interceptor hook method. In general the interceptor corresponds to the observer participant in the Observer pattern [GoF95], where its callback hook methods play the role of event-specific update methods. If the ‘per-event’ context object strategy described in implementation activity 3 (121) is applied, context objects can be passed as parameters to the concrete interceptor callback hook methods. These methods can return results or raise exceptions, in accordance with the policies described in implementation activity 6 (126).

In implementation activity 2.4 (120) we identified the interception group OutRequest. Below we illustrate a common interceptor interface for this interception group:

public interface ClientRequestInterceptor {
   public void onPreMarshalRequest
      (UnmarshaledRequest context);
   public void onPostMarshalRequest
      (MarshaledRequest context);
}

For each interception point associated with the OutRequest interception group, the ClientRequestInterceptor defines a separate hook method that is called back by the dispatcher at the appropriate interception point.

5 Specify the dispatchers. For each interceptor, define a dispatcher interface that applications can use to register and remove concrete interceptors with the concrete framework. In addition, this interface is used by the framework to dispatch concrete interceptors registered at interception points. Two sub-activities are involved:
5.1 Specify the interceptor registration interface. A dispatcher corresponds to the Observer pattern’s [GoF95] subject role. It implements a registration interface for interceptors, which correspond to the observer role. Applications pass a reference to a concrete interceptor to the registration method, which stores the reference in a container in accordance with the Manager pattern [Som97].

To implement different callback policies, an application can pass a dispatcher additional parameters. For example, it can pass a priority value that determines the invocation order when multiple interceptors are registered for the same interception point, as described in implementation activity 6 (126). The dispatcher returns a key to the application that identifies the registered interceptor uniquely. An application passes this key to the dispatcher when it removes an interceptor it registered previously.

To automate interceptor registration, and to hide its implementation, a concrete framework can implement helper classes that provide ‘no-op’ implementations of interceptor interfaces. The constructors of these classes register instances automatically with the concrete framework. Applications derive their concrete interceptor implementations from the appropriate helper class, override its methods and call the base class constructor to register their interceptors implicitly.

In general, a specific dispatcher can forward every occurrence of its corresponding event types from the concrete framework to the concrete interceptors that registered for these events. Dispatchers are therefore often implemented using the Singleton pattern [GoF95].

The methods defined in the following ClientRequestDispatcher class allow applications to register and remove ClientRequestInterceptor instances with the MiddleORB concrete framework:

public class ClientRequestDispatcher {
   // Interceptors are stored in a Java vector and called
   // in FIFO order.
   Vector interceptors_;
 
   synchronized public void
   registerClientRequestInterceptor
      (ClientRequestInterceptor i) {
      interceptors_.addElement (i); // Add interceptor.
}
 
   synchronized public void
   removeClientRequestInterceptor
      (ClientRequestInterceptor i) {
      // Remove interceptor.
      interceptors_.removeElement (i);
   }
   // …
}
5.2 Specify the dispatcher callback interface. When an interception event occurs the concrete framework notifies its dispatcher. When notified, a dispatcher invokes the corresponding hook methods of its registered concrete interceptors. A dispatcher often provides the same interface to the concrete framework that its associated interceptor provides to the dispatcher.

There are two reasons for this similarity:

  • It streamlines performance, by allowing a dispatcher to delegate event notifications to its registered interceptors efficiently, without transforming any parameters.
  • It localizes and minimizes the modifications required if the public interface of the dispatcher changes. An example of such a modification might be the addition of a new interception point to the interception group associated with the dispatcher callback interface. In this case an additional hook method would be added to the callback interface.

In MiddleORB the internal dispatcher ClientRequestDispatcher also implements the interface ClientRequestInterceptor:

public class ClientRequestDispatcher
   implements ClientRequestInterceptor { /* … */ }

The MiddleORB client concrete ORB framework can thus use the callback hook methods in this interface to notify the dispatcher about all events related to client requests.

6 Implement the callback mechanisms in the concrete framework. When an interception event occurs the concrete framework notifies the corresponding dispatcher. The dispatcher then invokes the hook methods of all registered concrete interceptor callbacks in turn. A mechanism is therefore needed to propagate events from the concrete framework to its dispatchers and from the dispatchers to the registered interceptors. This mechanism can be implemented by applying the Observer pattern [GoF95] twice.

The first application of the Observer pattern occurs whenever the concrete framework reaches an interception point. At this point it creates the appropriate context object and notifies the dispatcher about the occurrence of the event. In terms of the Observer pattern, the concrete framework is a subject that is observed by a dispatcher.

When the concrete framework notifies the dispatcher, it can either pass the context object as a parameter, or it can use a pre-allocated singleton context object that acts as an interface to the concrete framework. In the first strategy, all event-related information is encapsulated in the context object, while the second strategy requires the concrete framework to store all of the necessary information. The choice of strategy depends on the design of the concrete framework, as described in implementation activity 3.3 (123).

The second application of the Observer pattern occurs after the dispatcher is notified. At this point it iterates over all interceptors that have registered at this interception point and invokes the appropriate callback method in their interface, passing the context object as a parameter. The dispatcher is thus also a subject that is observed by concrete interceptors.

The dispatcher’s internal callback mechanism can be implemented with the Iterator pattern [GoF95]. Similarly, a dispatcher can apply the Strategy pattern [GoF95] to allow applications to select from among several interceptor callback orderings:

  • Simple invocation strategies include ‘first-in first-out’ (FIFO) or ‘last-in first-out’ (LIFO) ordering strategies, where interceptors are invoked in the order they were registered or vice-versa. When using the Interceptor pattern to implement a particular ‘interceptor stack’, a combined FIFO/LIFO approach can be used to process messages traversing the stack. On the client a FIFO strategy can be used to pass messages down the stack. On the server a LIFO strategy can be used to pass messages up the stack.
  • A more sophisticated ordering callback strategy dispatches concrete interceptors in priority order. In this strategy an application passes a priority parameter when registering a concrete interceptor with a dispatcher. When propagating an event, the dispatcher invokes interceptors with higher priorities first.
  • Another sophisticated callback strategy is based on the Chain of Responsibility pattern [GoF95]. If a concrete interceptor can handle the event that its dispatcher delivers, it returns the corresponding result. Otherwise it can return a special value or raise an exception to indicate it is not interested in intercepting the event. In this case the callback dispatching mechanism asks the next interceptor in the chain to handle the event. This progression stops after one of the interceptors handles the event.

If an interceptor encounters error conditions that prevent it from completing its work successfully, it can invoke exceptions or return failure values to propagate these errors to handlers. In this case the concrete framework must be prepared to handle these errors.

When a client concrete ORB framework processes a request it instantiates a context object, and notifies the corresponding dispatcher to iterate through the registered interceptors to call their appropriate event handling hook methods, such as onPreMarshalRequest():

public class ClientRequestDispatcher {
   // …
   public void
   dispatchClientRequestInterceptorPreMarshal
      (UnmarshaledRequest context) {
      Vector interceptors;
      synchronized (this) { // Clone vector.
         interceptors = (Vector)
            interceptors.clone ();
      }
      for (int i = 0; i < interceptors.size (); ++i) {
         ClientRequestInterceptor ic =
            (ClientRequestInterceptor)
            interceptors.elementAt (i);
         // Dispatch callback hook method.
         ic.onPreMarshalRequest (context);
      }
   }
   // …
}
7 Implement the concrete interceptors. Concrete interceptors can derive from and implement the corresponding interceptor interface in application-specific ways. A concrete interceptor can use the context object it receives as a parameter to either:
  • Obtain additional information about the event that occurred or
  • Control the subsequent behavior of the concrete framework, as described in implementation activity 3 (121)

The Extension Interface pattern (141) can be applied to minimize the number of different interceptor types in an application. Each interception interface becomes an extension interface of a single interceptor object. The same ‘physical’ object can thus be used to implement different ‘logical’ interceptors.

A client application can provide its own ClientRequestInterceptor class:

public class Client {
   static final void main (String args[]) {
      ClientRequestInterceptor myInterceptor =
         // Use an anonymous inner class.
         new ClientRequestInterceptor () {
         public void onPreMarshalRequest
            (UnmarshaledRequest context) {
            System.out.println
               (context.getObj () + “called”);
            // …
         }
         public void onPostMarshalRequest
            (MarshaledRequest context) { /* … */ }
      };
      ClientRequestDispatcher.theInstance ().
         registerClientRequestInterceptor
            (myInterceptor);
      // Do normal work.
   }
}

In this implementation the client’s main() method creates an instance of an anonymous ClientRequestInterceptor inner class and registers it with the singleton instance of the ClientRequestDispatcher class. Whenever the client concrete ORB framework encounters a client request event it notifies the dispatcher, which then calls back the appropriate hook method of the registered interceptor. In this example the interceptor just prints a message on the screen after a method is invoked but before it is marshaled.

Example Resolved

Applications can use the Interceptor pattern to integrate a customized load-balancing mechanism into MiddleORB. By using interceptors, this mechanism is transparent to the client application, the server application, and the ORB infrastructure itself. In this example a pair of concrete interceptors are interposed by the client application:

  • Bind interceptor. When a client binds to a remote object, the bind interceptor determines whether subsequent invocations on the CORBA object should be load balanced. All such ‘load balancing’ objects can be replicated [GS97] automatically on predefined server machines. Information on load balancing, servers, and available replicated objects can be maintained in the ORB’s Implementation Repository [Hen98] and cached within memory-resident tables. Information on the current system load can reside in separate tables.
  • Client request interceptor. When a client invokes an operation on a remote object, the client request concrete interceptor is dispatched. This interceptor checks whether the object is replicated. If it is, the interceptor finds a server machine with a light load and forwards the request to the appropriate target object. The algorithm for measuring the current load can be configured using the Strategy pattern [GoF95]. Client developers can thus substitute their own algorithms transparently without affecting the ORB infrastructure or the client/server application logic.

The following diagram illustrates the scenario executed by the client request interceptor after the bind interceptor has replicated an object that is load balanced on multiple servers:

This scenario involves three steps:

  • A client invokes an operation on a replicated object (1).
  • The client request interceptor intercepts this request (2). It then consults a table containing the object’s replicas to identify a server with a lightest load (3). The bind interceptor created this table earlier when the object was replicated.
  • The client ORB forwards the request to the server with a light load (4). The server’s ORB then delivers it to the object implementation residing on this server (5) and dispatches its operation (6).

Variants

Interceptor Proxy variant (also known as Delegator). This variant is often used on the server-side of a distributed system to intercept remote operations. The server concrete framework automatically instantiates a proxy [POSA1] to a local object implementation residing on the server. This proxy implements the same interfaces as the object. When the proxy is instantiated it receives a reference to the actual server object.

When a client issues a request, the server’s proxy intercepts the incoming request and performs certain pre-processing functionality, such as starting a new transaction or validating a security tokens. The proxy then forwards the request to the local server object, which performs its process operations in the context established by the proxy:

After the object processing is finished, the proxy performs any postprocessing that is needed and returns the result, if any, to the client. Both the client and the server object are oblivious to the existence of the interceptor proxy.

Single Interceptor-per-Dispatcher. This variant allows only one interceptor to register with a specific dispatcher. This restriction can simplify the pattern’s implementation when it makes no sense to have more than one interceptor, in which case there is no need for the concrete framework to retain a whole collection of interceptors.

In MiddleORB there could be an interceptor interface for changing the concrete framework’s transport protocol dynamically [Naka00]. At most there should be one interceptor that changes the default behavior of the concrete framework. Thus, there is no reason to register a chain of different interceptors that are each responsible for changing the transport protocol.

Interceptor Factory. This variant is applicable when the concrete framework instantiates the same class multiple times and each instance of the class is subject to interception. Instead of registering an interceptor for each object with the dispatcher explicitly, applications register interceptor factories with the concrete framework. Thus, for every object the concrete framework instantiates, it also instantiates a concrete interceptor using the supplied factory.

In MiddleORB there could be a different interceptor for each object implementation created by the server concrete ORB framework. In addition the client concrete ORB framework could use a factory to instantiate a separate client interceptor for each proxy.

Implicit Interceptor Registration. Rather than registering interceptors via dispatchers explicitly, a concrete framework can load interceptors dynamically. There are two ways to implement this strategy:

  • The concrete framework searches for interceptor libraries in predefined locations. It then loads these libraries into the concrete framework and ensures that they support the required interceptor interfaces before installing and dispatching events to them.
  • The concrete framework can link interceptors dynamically using a run-time configuration mechanism, such as the one defined by the Component Configurator pattern (75). In this design a component configurator component within the concrete framework interprets a script that specifies which interceptors to link, where to find the dynamically linked libraries (DLLs) that contain these interceptors, and how to initialize them. The component configurator then links the specified DLLs and registers the interceptors contained within them with the concrete framework.

Known Uses

Component-based application servers for server-side components, such as EJB [MaHa99], CORBA Components [OMG99a], or COM+ [Box97], implement the Interceptor Proxy variant. To help developers focus on their application-specific business logic, special concrete frameworks—often denoted as ‘containers’ in this context—are introduced to shield components from the system-specific run-time environment. Components need not implement all their infrastructural services, such as transactions, security, or persistence, but instead declare their requirements using configuration-specific attributes. The diagram below illustrates this container architecture:

After a new component is instantiated, the concrete framework also instantiates an interceptor proxy and associates it with that particular component, for example, by providing the proxy with a component reference during its initialization. After any client request arrives the proxy checks the configuration-specific attributes of the component and performs the services it expects, such as initiating new transactions.

Application servers often provide an instantiation of the standard Interceptor pattern to notify components about lifecycle events, such as connection initiation and termination, component activation and passivation, or transaction-specific events.

CORBA implementations [OMG98c] such as TAO [SLM98] and Orbix [Bak97] apply the Interceptor pattern so that application developers can integrate additional services to handle specific types of events. Interceptors enhance ORB flexibility by separating request processing from the traditional ORB communication mechanisms required to send and receive requests and replies.

For example, Orbix defines the concept of filters that are based on the concept of ‘flexible bindings’ [Shap93]. By deriving from a predefined base class, developers can intercept events. Common events include client-initiated transmission and arrival of remote operations, as well as the object implementation-initiated transmission and arrival of replies. Developers can choose whether to intercept the request or result before or after marshaling. Orbix programmers can leverage the same filtering mechanism to build multi-threaded servers [SV96a] [SV96b] [SV96c]. Other ORBs, such as Visibroker, implement the Interceptor Factory variant of the Interceptor pattern.

The OMG has introduced a CORBA Portable Interceptor specification [OMG99f] to standardize the use of interceptors for CORBA-compliant implementations. Portable Interceptors are intimately tied into the communication between a client and server. They can thus affect the contents of CORBA requests and replies as they are exchanged, as outlined in the following two examples:

  • A client-side security interceptor can add authorization information to a request transparently before it leaves the client process. The matching server-side security interceptor in the receiving server could then verify that the client is authorized to invoke requests on the target object before the request is dispatched. If authorization fails the request should be rejected.
  • A transaction interceptor is another example of a Portable Interceptor. This interceptor adds a transaction ID to a request before it leaves the client. The corresponding server-side transaction interceptor then ensures the request is dispatched to the target object within the context of that particular transaction.

Fault-tolerant ORB frameworks. The Interceptor pattern has been applied in a number of fault-tolerant ORB frameworks, such as the Eternal system [NMM99] [MMN99] and the CORBA Fault-Tolerance specification [OMG99g]. Eternal intercepts system calls made by clients through the lower-level I/O subsystem and maps these system calls to a reliable multicast subsystem. Eternal does not modify the ORB or the CORBA language mapping, thereby ensuring the transparency of fault tolerance from applications.

The AQuA framework [CRSS+98] also provides a variant of the Interceptor pattern. The AQuA gateway acts as an intermediary between the CORBA objects and the Ensemble group communication subsystem, and translates GIOP messages to group communication primitives. AQuA uses the Quality Objects (QuO) [ZBS97] framework to allow applications to specify their dependability requirements.

COM [Box97] [HS99a] programmers can use the Interceptor pattern to implement the standard interface IMarshal in their components. IMarshal provides custom marshaling functionality rather than standard marshaling, which is useful for several reasons. For example, custom marshaling can be used to send complex data such as graph structures across a network efficiently.

When the COM run-time system transfers an interface pointer from a component to a client residing in another execution environment, it queries the corresponding component for an implementation of the interceptor interface IMarshal. If the component actually implements IMarshal, the COM run-time uses the methods of this interceptor interface to ask the component for specific information to allow it to externalize the data to a stream object.

Web browsers. Web browsers implement the Interceptor pattern to help third-party vendors and users integrate their own tools and plug-ins. For example, Netscape Communicator and Internet Explorer allow browsers to register plug-ins for handling specific media types. When a media stream arrives from a Web server the browser extracts the content type. If the browser does not support the content type natively, it checks whether a plug-in has registered for it. The browser then invokes the appropriate plug-in automatically to handle the data.

The dynamicTAO reflective ORB [KRL+00] supports interceptors for monitoring and security. Particular interceptor implementations are loaded into dynamicTAO using component configurators (75). Using component configurators to install interceptors in dynamicTAO allows applications to exchange monitoring and security strategies at run-time.

Change of address surface mail forwarding. A real-life example of the Interceptor pattern arises when people move from one house to another. The post office can be instructed to intercept surface mail addressed to the original house and have it transparently forwarded to the new house. In this case, the contents of the mail is not modified and only the destination address is changed.

Consequences

The Interceptor pattern offers the following benefits:

Extensibility and flexibility. By customizing and configuring interceptor and dispatcher interfaces, users of a concrete framework can add, change, and remove services without changing the concrete framework architecture or implementation.

Separation of concerns. Interceptors can be added transparently without affecting existing application code because interceptors are decoupled from application behavior. Interceptors can be viewed as aspects [KLM+97] that are woven into an application, so that programmers can focus on application logic rather than on infrastructure services. The Interceptor pattern also helps to decouple programmers who write interceptor code from programmers who are responsible for developing and deploying application logic.

Support for monitoring and control of frameworks. Interceptors and context objects help to obtain information from the concrete framework dynamically, as well as to control its behavior. These capabilities help developers build administration tools, debuggers, and other advanced services, such as load balancing and fault tolerance.

When a client invokes a remote operation, an interceptor can be notified automatically. By using the context object the interceptor can change the target object specified in the method invocation from the original destination to another server that provides the requested service. The choice of server can depend on various dynamic factors, such as current server load or availability. If a framework cannot complete a request successfully, another interceptor can be activated to re-send the request to a replicated server that provides the same service, thereby enhancing fault tolerance via replication [OMG99a].

Layer symmetry. To implement layered services, developers can introduce symmetrical interceptors for related events exposed by the concrete framework. For example, in a CORBA environment developers could write a client-side interceptor that creates security tokens and automatically adds these tokens to outgoing requests. Similarly, they could write a symmetrical server-side interceptor that extracts these tokens before the incoming request is forwarded to the actual object implementation.

Reusability. By separating interceptor code from other application code, interceptors can be reused across applications. For example, an interceptor used to write information into a log file may be reused in other applications that require the same type of logging functionality.

The Interceptor pattern also incurs the following liabilities:

Complex design issues. Anticipating the requirements of applications that use a specific concrete framework is non-trivial, which makes it hard to decide which interceptor dispatchers to provide. In general, providing insufficient dispatchers reduces the flexibility and extensibility of the concrete framework. Conversely, providing too many dispatchers can yield large, inefficient systems that are complex to implement, use and optimize.

A similar problem arises when a concrete framework defines many different interceptor interfaces and dispatchers. In this case interceptor implementors must address all these heterogeneous extensibility mechanisms. If there are too many different mechanisms it is hard to learn and use them. In contrast, providing only one generic interceptor and one generic dispatcher can lead to bloated interfaces or complex method signatures. In general, it is hard to find the right balance without knowledge of common application usages.

Malicious or erroneous interceptors. If a concrete framework invokes an interceptor that fails to return, the entire application may block. To prevent blocking, concrete frameworks can use configurable timeout values. If the interceptor does not return control after a specified time, a separate thread can interrupt the execution of the interceptor. This approach can complicate concrete framework design, however.

For example, complex functionality may be required to help concrete frameworks recover from time-outs without leaking resources or corrupting important data structures. Interceptors can also perform unanticipated activities or cause run-time errors. It is hard to prevent these problems because concrete frameworks and interceptors generally execute in the same address space.

Potential interception cascades. If an interceptor leverages a context object to change the behavior of the concrete framework it may trigger new events, thereby initiating state transitions in the underlying state machine. These state transitions may cause the concrete framework to invoke a cascade of interceptors that trigger new events, and so on. Interception cascades can lead to severe performance bottlenecks or deadlocks. The more interceptor dispatchers that a concrete framework provides, the greater the risk of interception cascades.

See Also

The Template Method pattern [GoF95] specifies a skeleton for an algorithm—called the ‘template method’—where different steps in the algorithm can vary. The execution of these variants is delegated to hook methods, which can be overridden in subclasses provided by clients. The template method can therefore be viewed as a lightweight concrete framework, and the hook methods as lightweight interceptors. The Template Method pattern can be used to leverage interception locally at a particular level of abstraction, whereas the Interceptor pattern promotes interception as a fundamental design aspect that cuts across multiple layers in a framework architecture.

The Chain-of-Responsibility pattern [GoF95] defines different handlers that can be interposed between the sender and the receiver of a request. As with the Interceptor pattern, these handlers can be used to integrate additional services between senders and receivers. In the Chain-of-Responsibility pattern, however, requests are forwarded until one of the intermediary handlers processes the request. In contrast, a dispatcher in the Interceptor pattern usually forwards events to all concrete interceptors that have registered for it.

To emulate the Interceptor pattern, each intermediary handler in a chain of responsibility must therefore both handle and forward the request. Interceptor and Chain of Responsibility differ in two other aspects, however. Event handlers in a chain of responsibility are chained together, as the name of the pattern implies. In contrast, concrete interceptors in a framework need not be chained together, but can instead be associated at various levels of abstraction in a layered architecture [POSA1]. Event handlers in a chain of responsibility also cannot control the subsequent behavior of other event handlers or application components. Conversely, a key aspect of the Interceptor pattern is its ability to control a concrete framework’s subsequent behavior when a specific event occurs.

The Pipes and Filters pattern [POSA1] defines an architecture for processing a stream of data in which each processing step is encapsulated in a filter component. Data is passed through pipes between adjacent filters. If a concrete framework is structured as a Pipes and Filters architecture with clients and objects being the endpoints, each pipe in the Pipes and Filter chain defines a potential location at which interceptors can be interposed between adjacent filters. In this case, registration of interceptors consists of reconfiguring the Pipes and Filters chain.

The context object is the information passed from the source filter to the interceptor. The interceptor is responsible for sending information to the sink filter in the appropriate format. However, in the Pipes and Filters pattern, filters are chained via pipes, whereas in the Interceptor pattern concrete interceptors at different layers are often independent. In addition, Pipes and Filters defines a fundamental computational model for a complete application ‘pipeline’, whereas interceptors are used to implement ‘out-of-band’ services in any type of concrete framework.

The Proxy pattern [GoF95] [POSA1] provides a surrogate or placeholder for an object to control access to itself. Although proxies can be used to integrate additional functionality to a system, their use is restricted to objects that are already visible in a system. In contrast, interceptors allow external components to access and control internal and otherwise ‘invisible’ components. As described in the Variants section, to instantiate the Interceptor Proxy variant, we can instantiate the Proxy pattern with enhancements such as context objects.

The Observer [GoF95] and Publisher-Subscriber [POSA1] patterns help synchronize the state of cooperating components. These patterns perform a one-way propagation of changes in which a publisher can notify one or more observers/subscribers when the state of a subject changes. In contrast to the Interceptor pattern, the Observer and Publisher-Subscriber patterns do not specify how observers/subscribers should access the functionality of publishers because they define only one-way communication from the publishers to the subscribers. These patterns also emphasize event notifications, whereas the Interceptor pattern focuses on the integration of services into a framework.

These differences are also illustrated by the difference between event objects and context objects. While event objects often contain values related to the current event, context objects provide an additional programming interface to access and control concrete frameworks. The Observer and Publisher-Subscriber patterns can therefore be viewed as variants of the Interceptor pattern, in which context objects correspond to event types that are transferred from concrete frameworks playing the subject role to interceptors playing the observer/subscribe roles.

The Reflection pattern [POSA1] provides a mechanism for changing structure and behavior of software systems. A layer of base-level objects includes the application logic. An additional layer, the meta-level, provides information about system properties and allows developers to control the semantics of the base level. The relationship between the Reflection pattern and the Interceptor pattern is twofold:

  • Interception provides a means to implement reflective mechanisms. For example, to instantiate the Reflection pattern we can introduce dispatchers that help developers introduce new behavior by registering interceptors with the meta-level. Interception can thus be viewed as a lightweight reflective approach that is easier to implement and less consumptive of CPU and memory. Moreover, interception only exposes certain of the internals of the underlying system, whereas reflection often covers a broader scope.
  • Reflection can define a type of interception mechanism. The main intent of reflection is to allow applications to observe their own state so that they can change their own behavior dynamically. In contrast, the main intent of Interceptor is to allow other applications to extend and control the behavior of a concrete framework.

The Reactor pattern (179) demultiplexes and dispatches service requests that are delivered concurrently to an application from one or more clients. While the Reactor pattern focuses on handling system-specific events, the Interceptor pattern helps to intercept application-specific events. The Reactor pattern is often instantiated to handle system events occurring in the lower layers of a communication framework, whereas the Interceptor pattern is used in multiple layers between the framework and the application.

Credits

Thanks to Fabio Kon who contributed the dynamicTAO known use.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.31.209