UML软件工程组织

 

 

Framework Design Rules
 
2008-05-27 出处:cs.wustl.edu
 
This document describes design rules to follow while developing and using object-oriented frameworks for communication systems.

Table of Contents

1. Introduction
2. Programming Principles
3. Compiler Concessions
4. Configuration Concerns
5. Initialization Issues
6. Pattern Practices
7. Multi-threaded Matters
8. Real-time Rules
9. International Intents
10. Conclusions

1. Introduction

The OO application framework design rules described below have been derived by re-engineering the design, implementation, and proper use of the ACE C++ framework. ACE is an object-oriented (OO) framework that implements many core design patterns for concurrent communication software. ACE provides a rich set of reusable C++ wrappers and framework components that perform common communication software tasks across a range of operating system platforms.

2. Programming Principles

This section describes coding practices that directly result from practical framework design issues, including maintainability and reusability.
  • #guard header files
  • Avoid global functions
  • Respect user namespace
  • Use explicit destructor with placement new
  • Avoid -> chains
  • Avoid making direct system calls
  • Use consistent error handling
  • Implement dump() methods
  • Use abstractions for file descriptors
  • Allow for run-time tracing
  • Check all returned values
  • Qualify methods not in scope
  • Avoid data members in base classes
  • Implement open()/close() methods
  • #guard header files

    Rule Always guard against multiple inclusion of header files.

    Example Suppose we have files a.h, b.h, and c.h which declare classes A, B and C, respectively. Further suppose that B and C both inherit from A, and that C contains a reference to B. Hence, b.h includes a.h and that c.h includes both a.h and b.h.

    Rationale Without guarding against multiple inclusion, c.h from the example above will contain two declarations for class A.

    Applicability Any header file.

    Consequences Prevents multiple declarations. Requires additional code to be written. However, this can be "boiler-plated" by a suitable development environment or program editor.

    Exceptions There are no exceptions.

    Known Uses Applies to every header file.

    Enforcement Can be automated.

    Avoid global functions

    Rule Avoid the use of global functions. If functions are required, place them within a namespace or nested in a class.

    Example Integrating a framework like ACE with third party frameworks like X Windows and other class libraries like STL or Tools.h++ require callback functions to be defined and passed into them. For instance, thread creation functions require a C-style function:

    void *thread_entrypoint (void *arg)
    {
      // ...
    }
    
    // ...
      thr_create (0, 0, thread_entrypoint, param, 0, &tid);
    
    However, defining functions like my_callback at global scope can pollute the namespace. Namespace pollution makes it hard to integrate the various frameworks and class libraries. Therefore, all global functions in a framework should be preceded with a unique prefix. One way to do this is as follows:
    void *ace_thread_entrypoint (void *arg)
    {
      // ...
    }
    
    // ...
      thr_create (0, 0, ace_thread_entrypoint, param, 0, &tid);
    
    Often, a better way to structure this code is to scope it as a static method within a class, as follows:
    class ACE
    {
      static void *thread_entrypoint (void *arg)
      {
        // ...
      }
    };
    
    // ...
      thr_create (0, 0, ACE::thread_entrypoint, param, 0, &tid);
    
    This solution is more abstract, but may cause problems with certain C++ compilers (such as the IBM MVS C++ compiler) that don't allow static member functions to be used as parameters for functions that expect C functions (such as thr_create or signal).

    If a compiler supports namespaces, an even better way to structure this code is to scope it within a namespace, as follows:

    namespace ACE
    {
      void *thread_entrypoint (void *arg)
      {
        // ...
      }
    }
    
    // ...
      thr_create (0, 0, ACE::thread_entrypoint, param, 0, &tid);
    
    However, many older C++ compilers don't support namespaces yet, so this approach is less portable.

    Rationale Protecting global functions with namespace or class scoping, minimizes the possibility of colliding with user code.

    Applicability This rule should be applied whenever a C++ function is required.

    Consequences If the compiler lacks support for namespace, then scoping inside a class is acceptable but less convenient.

    Exceptions None

    Known Uses ACE precedes its handful of global functions with the ace_ prefix. Moreover, it places most stand-alone functions as static methods within the ACE class.

    Enforcement Can be automated.

    Respect user namespace

    Rule Avoid polluting the global namespace.

    Example Rationale This rule is a generalization of the Avoid global functions. It extends the application of the rule to global variables as well as functions.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    Use explicit destructor with placement new

    Rule All uses of the placement new operator must be accompanied by a corresponding explicit destructor call.

    Example Consider the following code fragment.

    class A *arr = Allocator::malloc (MAX * sizeof (class A));
    for (int i = 0; i < MAX; i++)
    new (arr + i) A (i);
    // ...
    Allocator::free (arr);
    

    Rationale If class A dynamically allocates memory for itself, the example above results in memory being leaked. In order to reclaim the memory, the destructor should be explicitly called.

    for (int i = 0; i < MAX; i++)
      (arr + i)->A::~A ();  // arr[i].A::~A (); is equivalent,
    
    Note that it is incorrect to attempt to use delete on any part of arr, since the the memory was not allocated with new.

    Applicability This still applies (but in reverse) if placement new is applied to an existing fully initialized object. That is, the destructor should be called explicitly first before placement new is applied.

    Consequences Faithfully applying this rule will lead to fewer problems with memory leaking. However, programmers may decide not to use general memory allocators to avoid the complexity of remembering to call destructors explicitly.

    Exceptions There are no exceptions.

    Known Uses In ACE, classes that allow for parameterizable allocators follow this rule.

    Enforcement Automatic enforcement imperfect without data-flow analysis.

    Avoid long pointer dereferencing chains

    Rule Avoid long chains of pointer dereference operators.

    Example Avoid lines of code that resemble a ()->b ()->c ()->d ()->e ();. Since actual objects names are usually longer than a single character, one can imagine such a chain being difficult to read.

    In addition, if the line of code is executed in a performance-critical section of the program many C++ compilers will not optimize this.

    Rationale The primary concern is that of code readability and maintainability. When debugging, the programmer is faced with tracing through number methods to understand the behavior of the code fragment. Efficiency is a secondary, but relevant, issue.

    Adherence to this rule can lead to framework designs that are more cohesive, and easier to understand.

    Applicability Everywhere.

    Consequences Makes method invocations easier to follow. A performance speedup may result by explicitly creating intermediate temporary pointers.

    May result in programmers manually creating temporaries before derefencing the next method call. However, these can often be optimized away by the compiler.

    Exceptions None.

    Known Uses ACE, TAO and JAWS.

    Enforcement Can be automated.

    Avoid making direct system calls

    Rule Framework users should always use wrappers instead of directly making system calls.

    Example Suppose a framework developer has need to create a temporary file with a name that can be passed to another process (i.e., the Standard C function tmpfile() is insufficient). In UNIX, the developer may be tempted to use the mktemp() system call, which creates a temporary file name that thus far does not exist. However, this system call is not available on all platforms (e.g., VxWorks and OSF1). Rather than making a direct system call, a wrapper should be used instead. For example,

    namespace ACE_OS {
      char *
      mktemp (char *s) {
    #if defined (ACE_LACKS_MKTEMP)
        // ... framework provided emulation
    #else
        return ::mktemp (s);
    #endif /* ACE_LACKS_MKTEMP */
      }
    }
    
    Thus, a temporary file can now be created with a call to ACE_OS::mktemp().

    Rationale This rule allows the framework to flexibly handle portability issues by emulating functionality when needed. This allows developers to program to a common API.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    Use consistent error-handling

    Rule The framework should employ and maintain a consistent error-handling interface.

    Example There are many styles of error-handling. Exceptions are beginning to become ubiquitous among C++ compiler implementations, but the quality of implementation across different vendors remains inconsistent. Another technique uses a error code return value, commonly used by system calls and synchronous method calls. Finally, classes that provide asynchronous methods may handle errors by providing a status method that reports the state of the asynchronous task.

    If a framework design lacks consistent error-handling, then semantically similar objects (such as containers) may present the framework user with different mechanisms for detecting error conditions. Which may be a potential source of confusion.

    Rationale Employing and deploying a consistent error-handling interface in the design of a framework can reduce the cost of maintenance, and raise its readability, and usability.

    Applicability Any exported class with methods whose execution may result in an error.

    Consequences It is admittedly difficult to choose a single error-handling interface that is flexible enough to handle all possible problems that may arise.

    Exceptions None.

    Known Uses JAWS Filecache and IO.

    Enforcement Code review.

    Implement dump() methods

    Rule Instrument a dump() method in every object.

    Example Suppose we are following the Allow for run-time tracing rule. It is often the case that the state of the object needs to be made known to create meaningful trace output.

    Rationale Following this design principle allows for easy inspection of the state of any object.

    Applicability Every object which maintains state that may need to be inspected at run-time.

    Consequences Enables run-time tracing. However, it may violate encapsulation unless the programmer has diligently provided accessor methods to the object's state.

    Exceptions Stateless objects.

    Known Uses ACE.

    Enforcement Can be automated.

    Use abstractions for file descriptors

    Rule Abstract away different representations of handles / file descriptors.

    Example Windows NT descriptors are represented as pointers, while in UNIX, they are represented as integers. If the framework is to be portable across both platforms, these differing representations need to be reconciled. In ACE, this reconcilation is achieved with ACE_HANDLE.

    #if defined (ACE_WIN32)
    typedef HANDLE ACE_HANDLE;
    #else
    typedef int ACE_HANDLE;
    #endif /* ACE_WIN32 */
    
    Here, HANDLE is the Win32 type name given for file descriptors, whose underlying representation is a void *.

    Rationale By applying an abstraction such as ACE_HANDLE to the representation of descriptors for both Windows NT and UNIX, framework developers can be more confident that the code can be ported to either platform with minimal disruption to application logic.

    Applicability Framework that performs operations on descriptors on multiple platforms.

    Consequences This technique may create a mandate that framework users program to the interface of the most exotic platform. For example, Windows NT uses a separate pointer type to represent network handles than from file handles, where as in UNIX they are both integers. In order to propogate this design rule, a new abstraction would be created that would become a network handle in Windows NT, and an integer in UNIX. This means framework users would be programming to the notion of separate handle types for file and network handles, even though they are the same type in UNIX.

    Exceptions Single platform environments.

    Known Uses ACE.

    Enforcement Can be automated.

    Allow for run-time tracing

    Rule Instrument framework methods to allow for run-time tracing.

    Example When developing applications, programmers often find the need to annotate portions of their program with print statements so as to be able to determine what state a program is in at the point of a failure. However, bare print statements are inconvenient after debugging has been completed, since all the trace statements have to be removed manually. To solve this problem, many developers employ a macro wrapper that contains the print statement, and can be selectively removed by redefining the macro to be blank.

    The ACE programming framework provides such tracing capabilities through its ACE_DEBUG macro. This flexible facility has the ability to send the trace messages to the screen, a file, or to a log server.

    Rationale Incorporating tracing mechanisms into the framework design provide aid to the debugging process of framework applications.

    Applicability Consequences When using macros that may be selectively removed, care has to be taken to not include expressions with side-effects to be taken as arguments into the macro.

    Exceptions Known Uses Enforcement Code review.

    Check all returned values

    Rule Either check or return all returned values from system functions and framework method calls.

    Example This rule is perhaps obvious, but it is quite an easy point to overlook. Developers often prototype code that they do not believe will be included in any actual product. This code can be, undestandably, sloppily written. However, when the prototype is shown to be useful, they often become included into the production code base. Perhaps the most common mistake is to not verify the result of a call to new.

    Rationale This can be considered an extension to the Use consistent error-handling rule. A program should consistently never assume all calls are successful.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    Qualify methods not in scope

    Rule Qualify references to base class methods and data. I.e., anything outside the scope of the class.

    Example Rationale Without a qualifying references, it is often not obvious to the code reviewer which function is being invoked. Qualifying these references increases the maintainability of the code.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    Avoid data members in base classes

    Rule Base classes should not store information about derived classes.

    Example Consider in ACE the Event_Handler. This abstract class is used as the base class for objects to be managed by a Reactor. Since the Reactor is a pattern based on the UNIX select() system call, an Event_Handler is associated with a HANDLE. However, storing the HANDLE associated with the Event_Handler in the abstract base class would be a mistake. The HANDLE is needed by the derived classes in order to perform their low-level actions, such as reading from and writing to the descriptor. The only way to ensure access would be to make the HANDLE data member public, or to provide public accessors to the data. However, a simpler solution is to create a virtual accessor method in the abstract class.

    Rationale Instead, the base class should provide virtual accessor methods to the information.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    Implement open()/close() methods

    Rule Use open() methods to initialize objects rather than constructors. Likewise, use close() methods to finalize objects rather than destructors.

    Example Suppose we are creating an object that with many different constructors. It is tedious to implement each constructor if many of the initializations are similar. The process can be simplified by implementing an open() method which performs the common initializations.

    Moreover, as indicated in the Initialize on first use rule, it is often useful to create static objects but to postpone their initialization (until the existence and initialization of their dependents can be verified). The existence of an open() method provides a mechanism for allowing this to take place.

    For destructors, consider the Use unguarded destructors rule. However, if shared data has been dynamically allocated, then it becomes necessary to serialize the contexts that attempt to release the allocated data. The presence of a close() method provides a callable method for which this can be done and allows the destructor to remain guard free.

    Rationale Creation and initialization are orthogonal issues. Likewise, destruction and finalization are orthogonal.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    3. Compiler Concessions

    This section presents rules that allow the framework design to flexibly cope the wide range of C++ compilers and their various levels of compliance with the C++ Standard.
  • Avoid using inline directly
  • Separate templates from non-templates
  • Use traits in templates
  • Use a non-template class as base
  • #guard template source files
  • Avoid using inline directly

    Rule Avoid using the inline keyword directly in C++ code. Instead, use a macro that can be automatically toggled to inline or not inline. Separate the inlines into their own implementation files, i.e., do not put inlined functions in header files.

    Example C++ inline functions are often useful to eliminate the invocation overhead of small, commonly used C++ methods at run-time. The following is a common way of writing this code:

    class ACE_SOCK_IO
    {
    public:
      ssize_t send (const void *buf, int n) const
      {
        // Call underlying OS function to send <buf>.
        return ::write (this->get_handle (), buf, buf_size);
      }
    
      // ...
    };
    

    Although this approach works, it is inflexible since it hard-codes the use of inlining into the framework and makes it difficult to separate the interface of a class from the implementations of its methods.

    It is often useful, however, to disable inlining when debugging a framework or debugging applications build using a framework for the following reasons:

    • Many debuggers get confused when they encounter inlined code. This makes it hard to step through the execution sequence of a program in the debugger.
    • If the code is always inlined, any changes to method implementations will cause all dependent object files to be recompiled.
    • Separating the interface from the implementation makes it easier to understand the documented semantics of a class.

    Therefore, it is often more useful to structure inlined C++ code as follows:

    // System-wide include file OS.h
    
    #if defined (__ACE_INLINE__)
    #define ACE_INLINE inline
    #else
    #define ACE_INLINE
    #endif /* __ACE_INLINE__ */
    
    // Header file SOCK_IO.h
    
    #include "ace/OS.h"
    
    class ACE_SOCK_IO
    {
    public:
      ssize_t send (const void *buf, int n) const;
      // ...
    };
    
    #if defined (__ACE_INLINE__)
    #include "ace/SOCK_IO.i"
    #endif /* __ACE_INLINE__ */
    
    // Include file SOCK_IO.i
    
    ACE_INLINE
    ssize_t
    ACE_SOCK_IO::send (const void *buf, int n) const
    {
      // Call underlying OS function to send <buf>.
      return ::write (this->get_handle (), buf, buf_size);
    }
    
    // Implementation file SOCK_IO.cpp
    
    #if !defined (__ACE_INLINE__)
    #include "ace/SOCK_IO.i"
    #endif /* __ACE_INLINE__ */
    

    Rationale By using a macro like ACE_INLINE in place of the inline keyword, the same source file can be used for building both debug and optimized versions of the framework. For debug versions, the macro can expand to an empty string, and the source compiled separately. For optimized versions, the macro can expand to inline, and be #include'd by the header file. This separation of concerns makes it easy to switch between debug and optimized configurations.

    If the inlined code has not been separated, then the change causes all dependent objects to be recompiled. In addition, by completely separating method implementation from method interfaces, header files can more clearly document the semantics of each method.

    Applicability Use this rule whenever you write C++ methods that should be easy to debug, as well as optimized.

    Consequences Editors that support source code parsing (for correct syntax and color highlighting) may get confused and parse the macro incorrectly. In addition, creating separate *.i files can increase the number of files that programmers must understand.

    Exceptions If a function is very short, will never fail, and should always be inlined, then it may make sense to use the inline keyword directly in the *.i file, as follows:

    // Include file SOCK_IO.i
    
    inline
    ssize_t
    send (const void *buf, int n) const
    {
      return ::write (this->get_handle (), buf, buf_size);
    };
    
    Note that it is still a good idea to have a separate include file, rather than including the method implementation in the class definition to enhance clarity and ensure flexibility for future changes.

    Known Uses The ACE framework uses this rule extensively.

    Enforcement Can be automated.

    Separate templates from non-templates

    Rule Template classes should not be mixed with non-template classes.

    Example Suppose a framework developer has created a template class, ACE_Task, which provides a general interface for active objects. The developer follows the Use a non-template class as base in order to allow references to this class to be passed as an argument to methods of other classes, such as the ACE_Thread_Manager. Then, the developer may be tempted to define the template and base at their point of use.

    #ifndef TASK_H
    #define TASK_H
    
    class ACE_Task_Base
    { // ...
    };
    
    template 
    class ACE_Task : public ACE_Task_Base
    { // ...
    };
    
    #endif
    
    However, upon compiling applications based on the framework, the developer finds the linker to complain about multiple definitions for ACE_Task_Base.

    Rationale This is a portability issue. Some compilers require that templates be in some accessible header file in order for the linker to resolve template instantiations. However, if mixed with non-template classes, multiple definitions may result.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    Use traits in templates

    Rule Use traits to merge multiple (related) template arguments.

    Example Consider the following example from the ACE framework. ACE defines an abstraction that can be used to parameterize allocation strategies applied by classes that need dynamic memory management. This abstraction is called ACE_Allocator. In order to facilitate the deployment of a variety of allocation strategies ACE provides a template called ACE_Malloc. This template is parameterized by MEMORY_POOL type, and by the locking strategy. Thus, enabling the allocation to be drawn from a static, dynamic, and/or persistent MEMORY_POOL. The type of MEMORY_POOL is typedef'd withing the ACE_Malloc class to create a trait. This trait is used when ACE_Malloc is specialized and applied to the ACE_Allocator_Adapter (an adapter template that subclasses from ACE_Allocator and delegates to a memory allocation strategy, such as ACE_Malloc). Without the use of the trait, MEMORY_POOL would have to be passed as a paramter the ACE_Allocator_Adapter.

    Rationale By employing traits, templates achieve greater consistency, are more reliable, and are easier to use.

    Applicability Consequences Exceptions Known Uses Enforcement Automation is probable.

    Use a non-template class as base

    Rule Try to have a base class that is not a template, from which specialized classes can be derived that may have template parameters.

    Example Rationale This rule also reduces the number of template parameters needed by allowing a non-template class provide the interface by which the template must follow. Thus, the instance of the template may be referenced though its base. See Use traits in templates.

    Applicability Consequences Exceptions Known Uses Enforcement Can be automated.

    #guard template source files

    Rule Templates may require source. Hence, template source files may need to be guarded against multiple inclusion too.

    Example Suppose we are following the Avoid using inline directly design rule, and have separated implementation code from the object declaration. Furthermore, the implementation code that can be inlined is held in their own separate file. The table below depicts the situation.
    class.h class.i
    #if !defined (CLASS_H)
    #define CLASS_H
    
    #include "ace/OS.h"
    
    class A
    {
    public:
      ACE_INLINE A (void);
      ~A (void);
    };
    
    #if defined (__ACE_INLINE__)
    #include "class.i"
    #endif /* __ACE_INLINE__ */
    
    #endif /* CLASS_H */
    
    #if !defined (CLASS_I)
    #define CLASS_I
    A::A (void)
    {
      // ...
    }
    #endif /* CLASS_I */
    
    class.cpp
    #include "class.h"
    
    #if !defined (__ACE_INLINE__)
    #include "class.i"
    #endif /* ! __ACE_INLINE__ */
    
    A::~A (void)
    {
      // ...
    }
    

    Rationale See #guard header files.

    Applicability All implementations of templates.

    Consequences Exceptions None.

    Known Uses All template classes and functions in ACE follow this design rule.

    Enforcement Can be automated.

    4. Configuration Concerns

    The issues addressed by these guidelines impact the design of frameworks with regard to how to ease porting tasks among different compiler, OS and hardware platforms.

  • #guard against compiler quirks
  • Specify a platform by its features
  • Use a config.h file
  • #guard against compiler quirks

    Rule Guard against quirky compiler implementations through #define abstractions (whereever possible).

    Example Some compilers require special keywords in order to make objects exportable in a dynamically linked library (MSVC++). Other compilers require explicit template instantiation. Different compilers may have different mechanisms to explicitly instantiate templates (e.g., #pragma instantiate, or via a special declaration form).

    Rationale The special keywords are only needed when the header file is used to build the library. When the user #include's the header file, the keyword should be ommitted. The use of a #define makes it possible to reuse the same header file for both framework developer and framework user.

    Applicability Files that are compiled on multiple platforms.

    Consequences Unless carefully done, may complicate things for code parsing tools.

    Exceptions Single platform environments.

    Known Uses ACE, JAWS, TAO.

    Enforcement Code review.

    Specify a platform by its features

    Rule Don't use conditional compilation based on compiler/OS/hardware platform, but upon available features.

    Example Rationale The conditional compilation preprocessing lines become descriptive. It is conceptually easier to port the framework to a new platform, since it only requires that the platform be described in terms of the features that are available.

    The alternative would be identify every conditional pre-processing line in the source and determine whether or not the platform under consideration requires to be added to the conditional or not.

    Applicability Any framework where portability is an issue.

    Consequences Eases porting task. If a new platform has a feature that none of the previous platforms have, then it can result in feature descriptions that are both positive (ACE_HAS_FEATURE) and negative (ACE_LACKS_FEATURE).

    Exceptions If portability is not an issue, rule does not apply.

    Known Uses ACE, JAWS, TAO.

    Enforcement Can possibly be automated.

    Use a config.h file

    Rule Centralize portability #ifdefs in a single place to ease portability maintainence.

    Example Suppose that the code for application App was written for UNIX, and is then to be ported to Windows NT, and it consists of many source files. It is a bad idea to visit each source file and add

        #ifdefs UNIX
        // ...
        #else
        // ...
        #endif
    

    Everywhere the code needs to change.

    Rationale The next platform that App needs to be ported to will require a re-visitation to each source file. If instead the incongruities are reconciled in a single place, then portability is enhanced by reducing the number of files that require changing.

    Applicability Any framework that depends upon portability.

    Consequences Ultimately, this leads to the creation of wrappers.

    Exceptions If the framework is never intended to be ported elsewhere, this rule does not apply.

    Known Uses ACE.

    Enforcement Might be automated.

    5. Initialization Issues

    Initialialization issues arise naturally in frameworks, since there are various objects for which only one instance should ever exist, known as singletons. Often, singleton classes will employ the use of static declarations for its own data members. This section warns about such practices.

  • Initialize on first use
  • Avoid creating statics
  • Provide destructors for singletons
  • Initialize on first use

    Rule Avoid the use of static/global objects whose constructors must be run in order to initialize the objects correctly. Instead, use Singletons that apply the Double-checked locking optimzation pattern.

    Example Suppose there is a static class of type A, that contains a data member that is a class of type B. Further, suppose that B contains static data.

    Rationale Since C++ does not guarantee the order of static initializations, unexpected results are possible if A depends upon the static values in B in its initialization.

    Applicability Use this rule whenever you develop class libraries or frameworks that require the use of objects that are logically global or static.

    Consequences It can be tedious to remove all uses of static and global objects from a large software system.

    Exceptions Static/global objects can be used correctly if all their initial values are 0 and/or they are simple pointers.

    Known Uses The ACE framework contains no unsafe static/global objects. Instead, it uses a single global Object Manager to control the lifetime of static/global objects.

    Enforcement Automation probable.

    Avoid creating statics

    Rule Do not make static objects whose correctness depends on constructors being called (i.e., if 0 initialization is not sufficient).

    Example Rationale See nostatics.

    Applicability Consequences Exceptions None.

    Known Uses Enforcement Automation probable.

    Ensure Singleton Destruction

    Rule Ensure Singleton destruction by providing hooks that delete Singletons before a process exits.

    Example Consider an editor application that uses a filemanager object to remember which files are opened in memory. Suppose this object is a singleton. The editor may need to maintain session information, so that the next editor invocation will have all the same files open as the last.

    Rationale An ideal time for the information to be recorded is before the editor actually exits. Since the information is contained in the filemanager, the destructor of the filemanager can record the necessary information so that the next invocation can open the files that had been open before.

    Applicability Any occurrence of a singleton object.

    Consequences Requires programmer to consider the possibility of singleton destruction.

    Exceptions None.

    Known Uses JAWS Filecache.

    Enforcement Automatic enforcement probable.

    6. Pattern Practices

    The design rules here encourage the use of particularly appropriate patterns that lend themselves to providing identifiable framework idioms. In general, these patterns particularly strengthen the skeleton nature of the framework while increasing the flexibility available to the application developer.
  • Strategize memory allocation
  • Apply the strategy pattern
  • Create a wrapper for similar functions
  • Parameterize components by wrappers
  • Implement iterators
  • Separate creation from use
  • Strategize Memory Allocation

    Rule Allow customization of memory allocation on a per-object or per-thread basis.

    Example Consider a real-time Object Request Broker, such as TAO. To reduce lock contention and avoid priority inversion, it is important to localize the scope of dynamic memory operations to memory pools allocated in thread-specific storage. Likewise, for medical imaging systems, it is often useful to allocate memory out of memory pools residing in shared memory to reduce data copying between separate image processing processes.

    One way to control of memory allocation/deallocation in C++ is to overload operators new and delete either globally or in a class-specific manner. However, this design is too general and can cause many unrelated parts of the system to behave incorrectly.

    A more flexible way of controlling memory allocation/deallocation in C++ is to define an Allocator component that can be parameterized in various ways, such as on a per-object or per-thread basis. For example, rather than saying:

    
    Image *create_image (bool use_shared_memory)
    {
      Image *image;
    
      if (use_shared_memory)
        {
          // ... mmap() file, obtain a pointer to shared memory
          // region, etc.
          image = new (shared_memory_pointer) Image (use_shared_memory);
        }
      else // use local memory.
        image = new Image (use_shared_memory);
    
      return image;
    }
    Developers can write
    
    Image *create_image (ACE_Allocator *allocator)
    {
      char *buf = allocator.malloc (sizeof Image);
      return new (buf) Image (allocator);
    }

    This design makes it possible to replace memory management strategies wholesale without affecting the application interfaces. In addition, it greatly reduces the effort required to keep track of which memory management strategies are associated with each object or thread.

    Rationale If memory allocation is a parameterized strategy, then it is straightforward to extend objects or threads to use different memory management policies transparently.

    Applicability This rule should be used whenever a framework requires fine-grained control over memory allocation and deallocation.

    Consequences Allows allocation strategies to be parameterizable. Requires generic allocation interfaces. Also, may require each object and their subobjects to be parameterized by a memory management strategy.

    Exceptions Known Uses ACE, JAWS, and TAO use parameterized memory management strategies extensively.

    Enforcement Code review.

    Apply the strategy pattern

    Rule Apply the strategy pattern to factor out common sources of variation in a component.

    Example Rationale A framework should provide enough structure as to form a skeleton application. Yet, it should provide enough flexibility so that application developers can do their job. Liberal application of the strategy pattern helps achieve this balance of structure and flexibility.

    Applicability Consequences Exceptions Known Uses Enforcement Automation unlikely.

    Create a wrapper for similar functions

    Rule Define wrappers around clusters of functionality that are semantically the same but may have accidental incompatibilities. E.g., semaphores, readers/writer locks, mutex + condition variables, which can be used by threads, processes, that may or may not reside on the same machine.

    Example Rationale Application of the adapter pattern. Eases usability of the framework by providing users with identical programming interfaces for similar OS services.

    Applicability Consequences Exceptions Known Uses Enforcement Automation unlikely.

    Parameterize components by wrappers

    Rule Define generic components that can be parameterized by the wrappers defined above (in the previous point).

    Example Rationale This enables components to keep their logic constant across different platforms.

    Applicability Consequences Exceptions Known Uses Enforcement Automation unlikely.

    Implement iterators

    Rule In searchable containers, separate traversal logic from item operations.

    Example Consider a container class that is used to maintain a dynamic priority queue of events. There are a number of operations which require examining all the items in the container. For instance, the insertion of a new event may require that the priorities of all events be re-evaluated. Without an iterator on the container, the priority queue is required to understand the underlying representation of the container class in order to implement the priority re-evaluator.

    Rationale Providing a method to iterate over items in a container insulates the user from the underlying representation of the container.

    Applicability Any searchable container.

    Consequences Permits entier contents of container to be inspected. Enables a dump() method to be more easily implemented.

    Exceptions None.

    Known Uses ACE containers.

    Enforcement Automation unlikely.

    Separate creation from use

    Rule Separate the creation of an object from its use.

    Example Suppose we are implementing a client/server system. Consider the server task of establishing a passive connection entry point which clients can use to negotiate a commnication channel to the server. There are details involved in this process, table lookups and structure initialization, that must be done before the passive connection is actually created.

    Rationale The abstraction is useful in situations where the logic behind the use of an object remains constant, but initialization of the object may be different across platforms.

    Applicability Consequences Exceptions Known Uses In ACE, this is applied to the Acceptor and Connector, as well as the Service Handler.

    Exceptions Automation unlikely.

    7. Multi-threaded Matters

    The use of these guidelines help framework designs avoid the common pitfalls present in the development of concurrent systems. Namely, race conditions and deadlocks. Also included are design rules that help avoid contention.
  • Make all methods reentrant
  • Avoid using sentinels
  • Use unguarded destructors
  • Guard open()/close methods
  • Use unguarded protected/private methods
  • Wait for spawned threads/processes
  • Avoid using reference counters
  • Make all methods reentrant

    Rule In multi-threaded objects, all methods should be re-entrant.

    Example Rationale If a method does not modify shared data, then concurrency is possible. Otherwise, mutual exclusion is required. Eitherway, race conditions are avoided.

    Applicability Consequences Exceptions Known Uses Enforcement Automation unlikely.

    Avoid using sentinels

    Rule Do not use sentinels or any other shared state that must be updated while searching a container that uses readers/writer locks.

    Example Containers are abstract data types (ADTs) that provide operations like bind/unbind, which insert and remove items in a container and find, which searches for an item in a container. Common implementations of these containers use linked data structures to implement hash tables, lists, and trees.

    In non-threaded applications, it is often possible to speed up container operations by using a sentinel. For instance, an implementation of a map that uses a hash table with ``bucket chaining'' to resolve collisions might be implemented as follows:

    template 
    class ACE_Hash_Map_Manager
    {
    public:
      ACE_Hash_Map_Manager (size_t table_size);
      // Constructor.
    
      int find (EXT_ID ext_id, INT_ID &int_id);
      // Returns the  if  is in the hash table.
    
      // ...
    
    private:
      struct ACE_Hash_Map_Manager_Entry
      {
         EXT_ID ext_id_;
         // External identify used as a key to find
         // the internal identifer.
    
         INT_ID ext_id_;
         // Internal identifier holds the value.
    
         ACE_Hash_Map_Manager_Entry *next_;
         // Points to the next entry in the overflow bucket.
      };
    
      ACE_Hash_Map_Manager_Entry **hash_table_;
      // Array of pointers to the linked list of entries.
    
      ACE_Hash_Map_Manager_Entry *sentinel_;
      // Sentinel node.
    
      LOCK lock_;
      // Synchronization strategy.
    };
    
    In a sentinel-based hash-table implementation, the constructor typically allocates a sentinel and initializes all the pointers in hash table to reference the sentinel, as follows:
    ACE_Hash_Map_Manager::ACE_Hash_Map_Manager
      (size_t table_size)
    {
      hash_table_ = new ACE_Hash_Map_Manager_Entry *[table_size];
      sentinel_ = new ACE_Hash_Map_Manager_Entry;
    
      // All pointers in the table initially point to the sentinel.
      for (size_t i = 0; i < table_size; i++)
        hash_table[i] = sentinel_;
    
    }
    
    Once the sentinel has been initialized, the find method can be optimized as follows:
    int
    ACE_Hash_Map_Manager::find (EXT_ID ext_id, INT_ID &int_id)
    {
      ACE_Read_Guard  mon (lock_);
      // This guard calls the acquire_read() method of class LOCK on
      // object lock_.
    
      // Initialize the sentinel with the value of
      // the entry we're trying to find.
      sentinel_->ext_id_ = ext_id;
    
      // Compute the hash value.
      size_t index = ext_id.hash ();
    
      // Find the beginning of the bucket chain.
      ACE_Hash_Map_Manager_Entry *entry = hash_table_[index];
    
      // Because we put the value we're looking for in the sentinel,
      // we don't need to check for a NULL pointer the end of the
      // chain since we'll always find the entry.
    
      while (entry->ext_id_ != ext_id)
        entry = entry->next_;
    
      // Determine if we really found the entry or just the sentinel.
      if (entry == sentinel_)
        return 0;
      else
        {
          int_id = entry->int_id_;
          return 1;
        }
    }
    
    Although this code will work correctly if given a ``NULL'' lock (such as ACE_Null_Mutex) it will fail if given a readers/writer lock (such as ACE_RW_Mutex). The problem is that a readers/writer lock only works correctly if the region of code it protects does not modify the state of the object. In this case, the sentinel_ maintains state that is shared by all threads that access the hash table. Therefore, the find can fail since there are race conditions if multiple threads concurrently update the sentinel_.

    Rationale Avoiding the use of sentinels in find operations reduces contention by removing the critical section around the storage of search value in the sentinel. In addition, it also avoids subtle race conditions that arise if the synchronization strategies are configured into parameterized types.

    Applicability Any searchable container class that uses readers/writer locks.

    Consequences One drawback to using this rule is that containers whose synchronization strategy is configured via parameterized types may not run as optimially when configured with "null" locking strategies.

    Exceptions Contains that will only be used in single-threaded applications need not follow this rule.

    Known Uses All ACE containers that support find operations and can be configured with readers/writer locks do not use sentinels.

    Enforcement Automation unlikely.

    Use unguarded destructors

    Rule The state of an object does not need to be guarded in destructors.

    Example Suppose class A has destructor A::~A. A::~A should only be performing idempotent actions that release resources.

    Rationale There is no purpose in creating unnecessary resource contention. If class A is holding a shared resource, then the resource itself should be guarded, not A.

    Applicability Consequences Destructors should not directly modify static memory if the action is not idempotent.

    Exceptions Known Uses Enforcement Automation unlikely.

    Guard open()/close() methods

    Rule Guard against multiple open() and close() calls, for idempotency.

    Example Assume the we have followed the open design rule so that we have separated object initialization from object creation. Suppose we have such an object A which happens to dynamically allocate memory upon initialization. Furthermore, A is referenced from multiple contexts (e.g., threads of control). It may happen that each context may attempt to initialize A if both concurrently view A as uninitialized. If A is unguarded, then a memory leak may result. Similar problems may occur in the finalization of A.

    Rationale The presence of guards would avoid race conditions. The use of the double check locking pattern can be used to achieve idempotent initialization and finalization.

    Applicability Every object that may be multiply referenced.

    Consequences Allows initilialization and finalization to be idempotent.

    Exceptions If object is never referenced by more than once, rule does not apply.

    Known Uses ACE.

    Enforcement Code review.

    Use unguarded protected/private methods

    Rule In classes that require locking, have public methods acquire the locks and call protected/private methods that do not acquire them.

    Example Consider a multi-threaded searchable container class. Typical methods include find, insert, and remove. Suppose, however, that the user requires the ability to atomically update an entry. Since the given remove and insert methods are themselves atomic, they cannot be used by update (otherwise, deadlock). Thus, adding an update method to the container class requires copying and pasting code from both remove and insert.

    This problem is resolved by providing private internal methods that perform the actual actions of finding, inserting and removing. These internal methods assume that locks are already held when they are called, and so do not require locking themselves. If these methods are called find_i, insert_i, and remove_i, then update can easily be implemented by applying remove_i followed by insert_i.

    Rationale Promotes reuse. Eases programming effort.

    Applicability Any data structure that requires synchronization in a multi-threaded framework.

    Consequences Exceptions Single threaded environments.

    Known Uses ACE containers, JAWS Filecache.

    Enforcement Code review.

    Wait for spawned threads/processes

    Rule Always remember to wait for the termination of spawned threads and/or processes in the main the thread before exiting.

    Example Rationale Not waiting may cause the system to lose resources that have not been properly released. For example, if threads hold references to shared memory resources (such as semaphore variables), these may not get released back to the system if the main thread exits prematurely. Another example, "zombie" processes take up process table entries.

    Applicability Consequences Exceptions Known Uses Enforcement Automation unlikely.

    Avoid using reference counters

    Rule Do not use explicit reference counters to maintain reference information on a shared multi-threaded object.

    Example In JAWS, cached file objects may be accessed by more than one thread. Since the object cannot be destroyed until all the threads that are currently referencing it are done, the number of current references need to be maintained. The most straight forward solution is to apply an explicit reference count. A better solution is to use readers/writer locks to maintain an implicit reference count.

    Rationale Since the reference count of an object is modifiable state that is shared among all threads accessing the object, acquiring and releasing the object requires 2 synchronization calls each. The amount of synchronization overhead can be cut in half by using an implicit reference count maintained by a readers/writer lock. Acquiring the object corresponds to a acquiring a reader lock, and releasing the object corresponds to releasing the reader lock. By trying to obtain a writer lock, it can be determined whether or not the object is no longer being referenced.

    Applicability Caches, references to automatic variables (smart pointers), all applications related to reference counting.

    Consequences Reduces the synchronization overhead required to access shared objects. If the object is a database object that has been updated, additional complexity is needed to properly release the object from the database.

    Exceptions Only applies in multi-threaded (and concurrent programming) environments. For single-threaded applications, reference counters are easier to program and maintain.

    Known Uses JAWS Filecache.

    Enforcement Code review.

    8. Real-time Rules

    The rules in this section describe promote framework designs that lead to the development of more correct and efficient framework components.
  • Avoid unequal priority sharing
  • Avoid unequal priority sharing

    Rule To minimize priority inversion, try not to share resources between threads of different priorities.

    Example Assume we are to implement a real-time system monitoring and control unit with 2 sensor inputs and a number of actuators. Suppose that one of the sensors supplies feedback at a higher rate than the other. Further assume the solution requires a separate thread to process the feedback from each sensor, and that a higher priority thread is used to process the input freom the high frequency sensor.

    One possible design is to have both sensor inputs be modeled as prioritized events that are fed into a single event queue, and each thread queries the the event queue for their respective sensor input. The problem with this approach is that it may happen that the higher priority thread may be blocked out by the lower priority thread if the lower priority holds a lock on the queue while a new sensor input has come in for the higher priority thread. This results in priority inversion.

    The better design is to dedicate a separte event queue for each thread, which removes contention for accessing the events.

    Rationale By not sharing resources between threads of different priorities, it is not possible for a lower priority thread to cause a higher priority thread to wait for the resource to be released.

    Applicability Consequences Exceptions Known Uses Enforcement Code review.

    9. Internationalization Intents

    As systems have to be ported to environments in which character sets are much larger than can be represented by 8 bits, internationalization of systems and frameworks can have a significant impact on design. These rules help reduce this impact.

  • Use an abstraction for char
  • Use an abstraction for char

    Rule Do not use the char type directly. Abstract it for wide characters.

    Example Suppose we are following the Create a wrapper for similar functions design rule. So, wrapper functions are created to utility functions that can operate on both regular char strings and wchar_t strings.

    In ACE, this is accomplished by creating overloaded wrapper functions for these utility functions. For example, ACE provides the following typedef and wrappers for these two static functions.

        #if defined (UNICODE)
        typedef char TCHAR
        #else
        typedef wchar_t TCHAR
        #endif
    
        namespace ACE
        {
          static int strcmp (const char *s, const char *t);
          static int strcmp (const wchar_t *s, const wchar_t *t);
        }
        

    Users of the framework can use TCHAR, and their code can then be easily ported to a UNICODE environment.

    Rationale This abstraction eases the task of porting applications to international environments.

    Applicability Consequences When using the abstraction for the char type, it is no longer valid assume that a character only occupies a single byte.

     Exceptions Known Uses Enforcement Can be automated.

    10. Conclusion

     

    组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

    京公海网安备110108001071号