This document describes
design rules to follow while developing and using object-oriented
frameworks for communication systems.
Table of Contents
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.
This section describes coding practices that directly result from
practical framework design issues, including maintainability and reusability.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
The rules in this section describe promote framework designs that
lead to the development of more correct and efficient framework components.
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.
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.
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.
|