I l @ ve RuBoard |
API developers are responsible for devising mechanisms that enable application developers to use any functionality that the API supports. Modern operating systems, file systems, protocol stacks, and threading facilities offer a wide range of capabilities. Today's APIs are therefore often large and complex, offering a myriad of functions that may require numerous arguments to select the desired behavior. Fortunately, the Pareto Principle, also known as the "80:20" rule, applies to software APIs; that is, most of what's commonly needed can be accomplished with a small subset of the available functionality [PS90]. Moreover, the same sets of functions are often used in the same sequence to accomplish the same or similar goals. Well-designed software toolkits take advantage of this principle by identifying and simplifying for these common use cases. The ACE wrapper facades simplify for common cases in the following ways. A.3.1 Combine Multiple Functions into a Single MethodProblem: C-level OS system function APIs often contain a number of functions that exist to support relatively uncommon use cases. As discussed in Section 2.3.2, for example, the Socket API supports many protocol families, such as TCP/IP, IPX/SPX, X.25, ISO OSI, and UNIX-domain sockets. To support this range of protocol families, the original Socket API designers defined separate C functions in the Socket API that
Therefore, creating and initializing a passive-mode Internet-domain socket requires multiple calls, as shown below: sockaddr_in addr; int addr_len = sizeof addr; int n_handle, s_handle = socket (PF_INET, SOCK_STREAM, 0); memset (&addr, 0, sizeof addr); addr.sin_family = AF_INET; addr.sin_port = htons (port); addr.sin_addr.s_addr = INADDR_ANY; bind (s_handle, &addr, addr_len); listen (s_handle); // ... n_handle = accept (s_handle, &addr, &addr_len); Many TCP/IP servers have a sequence of either these functions or a corresponding set that connect a socket actively. There are minor variations for how port numbers are selected, but it's basically the same code rewritten for every new TCP/IP-based application. As shown in Section 2.3.1, this repetition is a major source of potential programming errors and increased debugging time that all projects can do without. Making this set of operations reuseable in a simple and type-safe manner saves time and trouble for every networked application. Solution Combine multiple functions into a single method. This simplification alleviates the need for each project to (re)write tedious and error-prone code, such as the passive-mode connection initialization code shown above. The benefits of this approach increase with the number of lines of code that are combined. The specific calls needn't be made, which reduces the potential for errors in parameter usage. The main advantage of this approach, however, is the encapsulated knowledge of the set of functions to be called and the order to call them, which avoids a major source of potential errors. For example, the ACE_SOCK_Acceptor is a factory for passive connection establishment. Its open () method calls the socket() , bind() , and listen() functions to create a passive-mode communication endpoint. To achieve the functionality presented earlier therefore, applications can simply write the following: ACE_SOCK_Acceptor acceptor; ACE_SOCK_Stream stream; acceptor.open (ACE_INET_Addr (port)); acceptor.accept (stream); // . . . Likewise, the constructor of ACE_INET_Addr minimizes common programming errors associated with using the C-based family of struct sock-addr data structures directly. For example, it clears the sockaddr_in address structure (inet_addr_) automatically and converts the port number to network byte order, as follows : ACE_INET_Addr::ACE_INET_Addr (u_short port, long ip_addr) { memset (&this->inet_addr , 0, sizeof this->inet_addr_); this->inet_addr_.sin_family = AF_INET; this->inet_addr_.sin_port = htons (port); memcpy (&this->inet_addr_.sin_addr, &ip_addr, sizeof ip_addr); } In general, this approach yields code that's more concise , less tedious, and less error prone since it applies the Wrapper Facade pattern to avoid type-safety problems. A.3.2 Combine Functions Under a Unified Wrapper FacadeProblem: Today's computing platforms often supply some common types of functionality, but provide access to them quite differently. There may simply be different function names that do essentially the same thing. There may also be different functions altogether, which must be called in different orders on different platforms. This shifting set of core APIs and semantics makes it hard to port applications to new platforms. Multithreading is a good example of how APIs and semantics change across platforms. The steps to create a thread with different attributes varies widely across POSIX threads (Pthreads), UNIX International (UI) threads, Win32 threads, and real-time operating systems. The following list describes some of the key differences:
Solution Combine functions under a unified wrapper facade to properly manage the changes required when porting to different platforms. To deal with the multiple platform requirements of thread creation, for instance, the ACE_Thread_Manager::spawn () method accepts all its needed information via arguments (see Section A.3.3 for an important principle regarding arguments) and calls all of the OS system functions in the proper order. The different return value conventions of the platforms are accounted for and unified into one return convention used throughout ACE, that is, 0 on success, l on failure with the failure reason stored in errno . This solution allows all thread creation in ACE itself, and in user applications, to be accomplished with a single method call across all platforms. Your challenge when designing unifying functions is to choose the granularity of the interface to expose, and which low-level functions to combine. Keep these points in mind when deciding:
A.3.3 Reorder Parameters and Supply Default ValuesProblem: OS libraries contain system functions that must address a wide variety of anticipated uses, and allow access to all of a system's functionality. These functions therefore often have many arguments that are seldom used, but whose default values must be supplied explicitly. Moreover, the order of parameters in a function prototype doesn't always match the frequency with which applications pass nondefault values to the function, which increases the likelihood of coding errors. OS libraries are also often implemented in C so they can be called from a variety of languages, which prevents the use of language features and design abstractions that could help to alleviate these problems. For example, the UI threads thr_create() function takes six parameters:
Since most applications want default stack semantics, parameters 1 and 2 are usually 0. Yet, developers must remember to pass in the 0 values, which is tedious and error prone. It's also common for concurrent applications to spawn threads with "joinable" semantics, which means that another thread will rendezvous with and reap the thread's status when it exits. So parameter 5 has a common value, but it does change depending on the use case. Parameters 3 and 4 are the most commonly changed values. Parameter 6 is set by thr_create() , so it must be supplied by the caller. Solution Reorder parameters and supply default values in order to simplify for the most common cases. When designing wrapper facade classes, you can take advantage of two important factors:
From the problem illustrated above, you can reorder the parameters to put commonly used parameters first and seldomly used parameters at the end, where you can give them default values. For example, the ACE_Thread_Manager::spawn() parameters are ordered so that the most frequently changing parameters, such as the thread function and its void * argument, appear first. Default values are then given for the other parameters; for example, the default thread synchronization behavior is THR-JOINABLE. As a result, most applications can just pass the minimum amount of information necessary for the common case. C++ default parameters can be used for other purposes, as well. For instance, the connect() method in the ACE_SOCK_Connector class has the following signature: int ACE_SOCK_Connector::connect (ACE_SOCK_Stream &new_stream, const ACE_SOCK_Addr &remote_sap, ACE_Time_Value *timeout = 0, const ACE_Addr &local_sap = ACE_Addr::sap_any, int reuse_addr = 0, int flags = 0, int perms = 0, int protocol_family = PF_INET, int protocol = 0); In contrast, the ACE_TLI_Connector 's connect() method has a slightly different signature: int ACE_TLI_Connector::connect (ACE_TLI_Stream &new_stream, const ACE_Addr &remote_sap, ACE_Time_Value *timeout = 0, const ACE_Addr &local_sap = ACE_Addr::sap_any, int reuse_addr = 0, int flags = O_RDWR, int perms = 0, const char device[] = ACE_TLI_TCP_DEVICE, struct t_info *info = 0, int rw_flag = 1, struct netbuf *udata = 0, struct netbuf *opt = 0); In practice, only the first several parameters of connect() vary from call to call. To simplify programming, therefore, default values are used in the connect() methods of these classes so that developers needn't provide them every time. As a result, the common case for both classes are almost identical. For example, ACE_SOCK_Connector looks like this: ACE_SOCK_Stream stream; ACE_SOCK_Connector connector; // Compiler supplies default values. connector.connect (stream, ACE_INET_Addr (port, host)); // . . . and ACE_TLI_Connector looks like this: ACE_TLI_Stream stream; ACE_TLI_Connector connector; // Compiler supplies default values. connector.connect (stream, ACE_INET_Addr (port, host)); // ... The common signature provided by default parameters can be used in conjunction with the parameterized types discussed in [SH] to enhance generative programming [CE00] and handle variability using parameterized types, as described in Section A.5.3. A.3.4 Associate Cohesive Objects ExplicitlyProblem: Due to the general-purpose nature of OS-level libraries, there are often dependencies between multiple functions and data structures. Since these dependencies are implicit, however, they are hard to identify and enforce automatically. Section 2.3.1 illustrates this using the relationship between the socket() , bind() , listen() , and accept() functions. This problem is exacerbated when multiple data objects are sometimes used together, and other times used alone. For example, when using threading facilities, a mutex is a common synchronization mechanism. A mutex is also used together with a condition variable; that is, a condition variable and a mutex are usually associated for the condition variable's lifetime. The Pthreads API is error prone in this regard because it doesn't make this association explicit. The signatures of the pthread_cond_*() functions take a pthread_mutex_t* . The syntax alone therefore doesn't denote the tight coupling between a pthread_cond_t and a specific pthread_mutex_t . Moreover, the Pthread condition variable API is tedious to use because the mutex must be passed as a parameter every time the pthread_cond_wait() function is called. Solution Associate cohesive objects explicitly to minimize the details that application developers must remember. Your job as a wrapper facade designer is to make the class user's job easy to do correctly and make it inconvenient (or impossible ) to do it wrong. Designing one class that encapsulates multiple, related objects is one technique for accomplishing this goal because it codifies the association between cohesive objects. This principle goes a step further than the use of wrapper facades to improve type safety described in Section A.2. The goal there was to apply the C++ type system to ensure strong type-checking. The goal here is to use C++ features to enforce dependencies between strongly typed objects. For example, to enforce the association between a condition variable and its mutex from the example above, the ACE_Condition_Thread_Mutex class described in Section 10.6 on page 229 contains both a condition variable and a mutex reference: class ACE_Condition_Thread_Mutex { // .... private: ACE_cond_t cond_; // Condition variable instance. ACE Thread Mutex &mutex ; // Reference to a mutex lock. }; The constructor of this class requires a reference to an ACE_Thread_Mutex object, forcing the user to associate the mutex correctly, as shown below: ACE_Condition_Thread_Mutex::ACE_Condition_Thread_Mutex (const ACE_Thread_Mutex &m): mutex_ (m) (/* ... */} Any other attempted use will be caught by the C++ compiler and rejected. |
I l @ ve RuBoard |