A.3 Simplify for the Common Case

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 Method

Problem: 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

  1. Create a socket handle

  2. Bind a socket handle to a communication endpoint

  3. Mark a communication endpoint as being a "passive-mode" factory

  4. Accept a connection and return a data-mode handle

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 Facade

Problem: 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:

  • Function names The function to spawn threads is named pthread_create() in Pthreads, thr_create() in UI threads, and Create_Thread() in Win32.

  • Return values Some threading APIs return a thread ID (or handle) on success, whereas others return 0. For errors, some return a distinct value (with the error code stored elsewhere) and others return an error code directly.

  • Number of functions To specify thread attributes, such as stack size or priority, all of the attributes are passed to CreateThread() on Win32 or thr_create() on UI threads. In Pthreads, however, separate functions are used to create, modify, and later destroy thread attributes data structures, which are then passed to the pthread_create() function.

  • Order of function calls When multiple function calls are required, they're sometimes required to be in different orders. For example, the Pthreads draft 4 API requires a thread to be marked joinable after it's created, while other Pthreads implementations require it before.

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:

  • Portability The wrapper facade method should strive to offer the same semantics across all platforms. This isn't always possible; for example, not all OS platforms allow creation of system-scope threads, as discussed in Section 5.4, In many cases, however, the method can mask these differences by emulating unsupported features or ignoring requested features or attributes that aren't essential. See Section A.5 for more guidance on this point.

  • Ease of use The caller should be able to identify how to use your wrapper facade method, what preparations need to be made before calling, such as creating attributes, and what are the return values and their meanings. Having a small number of methods is often easier to use than a larger number of related methods. In addition, beware of side effects, such as internal memory allocation, that callers must remember to handle.

A.3.3 Reorder Parameters and Supply Default Values

Problem: 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:

  1. Stack pointer

  2. Stack size

  3. Entry point function

  4. void * argument for the entry point function

  5. Flags used to create the thread

  6. Identifier of the created thread

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:

  • You know the common use cases and can design your interface to make the common cases easy for application developers to use.

  • You can specify and use an object-oriented implementation language, such as C++, and take advantage of its features and the higher-level abstractions enabled by object-oriented design.

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 Explicitly

Problem: 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


C++ Network Programming
C++ Network Programming, Volume I: Mastering Complexity with ACE and Patterns
ISBN: 0201604647
EAN: 2147483647
Year: 2001
Pages: 101

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net