Stephen H. Edwards
Dept. of Computer Science
Virginia Tech
660 McBryde Hall, MS 0106
Blacksburg, VA 24061 USA
edwards@cs.vt.edu
Phone: +1 540 231 5723
Fax: +1 540 231 6075
URL: http://people.cs.vt.edu/~edwards/
Defensive programming practices have not kept up with the evolution of current programming languages or techniques. While appropriate for use by the original developer, embedded assertions are difficult to use and control for clients of reusable components distributed in binary form--a perspective that is increasingly important with the rise of component-based development. A new strategy for using run-time assertion checks based on design by contract is proposed where checking code is placed in a separate wrapper class that can easily be inserted or removed. Use of a factory pattern for component creation allows client code to remain unchanged when switching between enabled or disabled checking (typically without requiring recompilation). There is no run-time penalty for checks when they are disabled, yet full control over when to enable checks--and how thoroughly checking is performed--can be left up to the client of the component. Further, both the checking wrapper and the underlying component can be distributed in compiled form, giving the client control without requiring source code access or recompilation of the component itself.
Keywords
Defensive programming, design by contract, wrapper class, decorator, unit test, precondition, postcondition, invariant, object-oriented programming, debugging aids
Paper Category: technical paper
Emphasis: research
Defensive programming is a well-known coding practice with a 30-year history [Parnas72]; it has been taught to programmers for decades. The core idea is this: if a routine or module depends on certain assumptions in order to operate correctly, add code to check that those assumptions hold at run-time. When the assumptions reflect how "correct" clients should interact with your software, such "defensive" checks protect your code from incorrect or inappropriate usage.
Within the realm of object-oriented programming, Bertrand Meyer’s concept of design by contract [Meyer92, Meyer97] lays out a clear division of responsibilities between a class implementation and its clients regarding what each party may assume and what each party is obligated to ensure. Phrasing interface contracts as method preconditions, method postconditions, and class invariants, either informally or using some model-based specification approach [Wing90], is one way to clearly and precisely define both sides of the contract between a component and its clients.
Unfortunately, while programming languages and design methods have
evolved since the early 70’s, mainstream practice regarding how to
provide run-time checking of interface contracts in the spirit of
defensive programming has changed little. Generally speaking,
programmers still write Boolean tests to check preconditions (well,
those that are easy or cheap to check, perhaps) and insert this code
at the start of method bodies. A preprocessor or a specially designed
tool may be used to conditionally include or exclude this checking
code during the build process. This strategy is typified by the use
of assert
macros in C++ or similar utility classes in
Java.
Run-time assertion checks do provide proven benefits [Voas97, Edwards00]. Unfortunately, the traditional approach of conditionally including assertion checking directly within class methods is most useful to the original developers of a component--those who have direct access to its implementation source code and can easily recompile it with alternative settings. Indeed, the general philosophy when using assertions is to enable them during development and testing, but disable or remove them for deployment.
That strategy is more difficult to manage for clients or reusers of such a component--particularly when the component has been purchased commercially or is distributed in compiled form only. In this case, the original developer may complete development of and release a software component, which may then be used by many other developers in creating a variety of software applications. Of course, these component clients can still benefit greatly from assertion checks in the original component; after all, defensive checks are powerful tools for detecting incorrect uses of a component by errant clients.
The central problem is this: when assertion checks are embedded directly within object methods, either one must pay some run-time penalty for the checks, even when they are not enabled, or one must recompile the methods without checks to eliminate this overhead. For the original developer, either option is available. For later reusers, unless source code is available, they must live with the choices of the original developer (who typically removes checks).
Component-based development and reuse of commercially or publicly distributed software parts is rapidly becoming the norm. This article proposes a different approach to providing run-time precondition, postcondition, and invariant checks using violation checking wrappers. Rather than placing checking code inside the component, such a wrapper isolates checks in a separate layer between the component and its client(s). This approach, which is more in-line with object-oriented design, addresses the shortcomings of including the checks in the component itself and delivers the following benefits:
Checking code is completely separated from the underlying component.
Checks are completely transparent to both the client and the component.
Checks are transformed into a separate component that can be distributed in compiled form along with the underlying component.
Component reusers can easily control the insertion or removal of checks without requiring recompilation of the underlying component, the checking code, or the client code.
When a wrapper is removed (to turn off checks), there is no run-time cost to the underlying component.
The approach does not require a preprocessor, a separate tool, or a language extension, and will work in most object-oriented languages.
The remainder of the article describes the basics of violation checking wrappers, gives an example of their use, and discusses possible extensions and enhancements. In addition, the relationship of this approach to previous work is also presented.
The basic idea at the heart of violation checking wrappers is simple: place the component under consideration in a wrapper, or decorator [Gamma95], that takes on the responsibility for performing defensive checks. To support the full breadth of design by contract, the wrappers presented here will support checks for preconditions, postconditions, and invariant assertions. Such a wrapper provides exactly the same syntactic interface as the component itself, and delegates the work involved in carrying out each operation to the component held inside. Figure 1 illustrates this simple idea.
Placing checking code in a separate class or component is a simple
idea, but it refocuses attention with dramatic results. Assertions
are no longer embedded in the original implementation, making the
original simpler and cleaner while alleviating fears that assertion
code causes too much clutter. Further, when checks are promoted to
the level of a separate artifact, it becomes easier for a programmer
to write longer, more thorough checking code than will typically fit
into an assert
macro or procedure call. More thorough
assertions lead to easier testing and better defect observability
[Voas97].
Perhaps more importantly, however, placing checking code into a separate class elevates the checks from the level of individual statements inside a method up to the level of a useful component abstraction. Managing all of a component’s checks as a separate class makes it easier to insert or remove them together, and makes it easier to parameterize the entire group by other decisions. For example, instead of hard coding one action when a check fails--say, printing a message and terminating the program--one can introduce a separate notification function (or class) as a parameter to the checking wrapper. This notification action can be a template parameter in languages that support generic programming, or a run-time parameter in other languages.
If the run-time checks are separated from the underlying component implementation, how will those run-time checks be implemented? Allowing the checking code to have direct access to the internal data stored within the underlying component provides the closest match to directly embedded checks. This simple approach will be explored here; elsewhere, this approach has been compared to more sophisticated alternatives that do not require direct access to internal component state [Edwards98].
Implementing a direct access checking wrapper is straightforward: simply move any violation checks one would normally place inside the methods of the underlying component into a new class, and ensure that within this new class, all internal data members in the underlying implementation are visible.
Just as one can separate the checking code from the base component, one can also separate different kinds of checks into separate classes. Rather than providing a single class that performs all checking, the most obvious division is to provide three different classes: one to check invariant properties, one to check preconditions, and one to check postconditions. This division of labor allows one to easily select which checks to perform by adorning the underlying component with a different form of decorator.
The technique proposed here will work in virtually any object-oriented setting, with components that range from individual classes in a programming language to more full-fledged CORBA, Java Beans, or COM components. To provide a concrete illustration, the examples in this paper will be presented in terms of Java classes for simplicity.
The violation detection wrapper strategy is simple and easy to use from the client’s point of view. To use a component, one simply needs an interface (corresponding to the component’s public contract) and a factory for creating new instances. For a Java class C, creating and manipulating objects is done in the normal way:
C my_c = CFactory.instance().newC( constructor-parameters ); my_c.aMethod( ... );
Since construction of new objects is handled through the factory pattern [Gamma95], decisions about whether or not to use wrappers (or which checks to enable) can be controlled and localized elsewhere. The example presented in Section 3 shows a simple way to provide programmatic control over factory preferences, although many other options (controlling wrapper choices via a separate initialization file, via a control panel user interface, or via a development tool’s property sheet feature) are straightforward.
Given the simplicity of the client perspective when using wrapped
components, what must the developer do to provide these capabilities?
The most obvious answer is to start with a simple decorator pattern,
as shown in Figure 2 (based on
[Gamma95]). If one
is working in Java, place the wrapper in the same package as the
underlying component implementation and ensure that all data members
are declared without any access modifier or as protected
.
This will give the wrapper visibility of the data members in the
underlying implementation in order to carry out checks.
Unfortunately, this simple arrangement has a number of limitations. First, it does not account for the factory services needed to create objects. Second, it does little to provide a uniform way to control what action(s) are taken in response to failed checks. Third, this approach does not immediately provide a way to selectively enable or disable certain classes of checks. Fourth, if the underlying component is subclassed later, it can difficult to reuse the wrapper for the parent class in creating the subclass later unless care is taken with the internal design of the wrapper class.
To address these issues, Figure 3 presents a more detailed "micro-architecture" for checking wrappers. Only the public component interface and the factory class(es) are needed by client code. The center of Figure 3 consists of the core decorator pattern, with one slight modification. Here, the violation detection wrapper performs checks specific to the implementation of a particular concrete component, and thus wraps that concrete class rather than any instance of the public component interface. It is possible instead to mimic the decorator pattern exactly, providing one violation detection wrapper that performs checking for any implementation of the public component interface (given suitable supporting classes), but at the expense of additional type casts.
The abstract factory for creating new instances appears on the left of Figure 3. The factory implementations provide a way to create wrapped or unwrapped instances of the component for use by client code.
The right side of Figure 3 introduces
several new classes that serve to partition the work performed by the
wrapper. Instead of directly embedding the run-time checking code
within the wrapper, these checks have been partitioned into three
helper classes: one each to perform precondition, postcondition, and
invariant checks. In addition, a new CheckListener
interface has been introduced to serve as a point of separation
between the checks being performed (the responsibility of the wrapper)
and the action(s) to take based on the result of a check (the
responsibility of the CheckListener
).
The end result of this structural design is that the wrapper itself has a simple, regular structure that is directly driven by the component’s public interface. Further, when one creates a subclass of the underlying component, subclasses of the three checking helper classes are easy to create and the same base wrapper can still be used. Finally, careful construction of the wrapper itself will allow a factory to create a wrapped object that carries any combination or subset of the three checking helper classes, in order to tailor the degree of checking provided.
To serve as an example base component, Figure 4 presents a simplified interface for a
Java Vector
. This interface is inspired by the
java.util.Vector
class, but only includes a core set of
methods for brevity and simplicity. A Vector
embodies a
growable array of objects that can be accessed by a numeric index. A
Vector automatically
increases in size to accommodate new elements as they are added. One
item of a Vector’s state is a capacity increment--that is, the amount
to "grow by" when the object needs to resize to accommodate a newly
inserted entry.
public interface Vector { void insertElementAt( Object obj, int index ); Object elemetAt ( int index ); void setElementAt ( Object obj, int index ); void removeElementAt( int index ); int size(); int capacity(); Object clone(); } |
Figure 4. A simplified Vector
interface.
Figure 5 sketches a possible
implementation of this Vector
interface using an array of
Object
references. The VectorArrayImpl
class borrows heavily from the java.util.Vector
implementation in Sun’s Java SDK (with simplifications) and is meant
to allow readers familiar with such vector implementations to apply
their existing understanding in the context of this example. The
implementation of only one method is shown for brevity.
class VectorArrayImpl implements Vector { protected Object[] elementData; protected int elementCount; protected int capacityIncrement; public void insertElementAt(Object obj, int index ) { ensureCapacityHelper( elementCount + 1 ); System.arraycopy( elementData, index, elementData, index + 1, elementCount - index ); elementData[index] = obj; elementCount++; } private void ensureCapacityHelper( int minCapacity ) { ... } // ... more implementation code here ... } |
Figure 5. A simplified Vector
implementation.
The VectorArrayImpl
class has only three data members:
elementData
is an array of Object
references; elementCount
stores the number of objects
currently contained in the Vector
(the number of slots
used in the array); and capacityIncrement
determines how
much additional space is requested when the Vector
must
grow. The insertElementAt()
method shown in
Figure 5 is
typical of most Vector
methods. This method inserts a
new object at the given zero-indexed location in the
Vector
, "pushing" all objects at that index and beyond up
by one to make room. It uses ensureCapacityHelper()
(whose implementation is not shown) to resize the underlying array, if
necessary.
Figure 6 illustrates the basic structure
of a violation detection wrapper designed using this approach. The
four data members in the wrapper maintain references to the wrapped
concrete component and the three checking helper objects. The
constructor of the wrapper is used to initialize these data members.
Figure 6 only shows the implementation of one
wrapped method:
insertElementAt()
. All other public methods are wrapped
in exactly the same way.
class VectorArrayImpl_Wrapper implements Vector { protected VectorArrayImpl vector; protected VectorArrayImpl_PreChecks pre_check; protected VectorArrayImpl_PostChecks post_check; protected VectorArrayImpl_InvCheck check; protected VectorArrayImpl_Wrapper( VectorArrayImpl v, VectorArrayImpl_PreChecks pre, VectorArrayImpl_PostChecks post, VectorArrayImpl_InvChecks inv) { vector = v; pre_check = pre; post_check = post; check = inv; // Check that invariant holds on (presumably newly constructed) vector if ( check != null ) { check.invariant( vector ); } } public void insertElementAt(Object obj, int index ) { // Checks before the method is executed if ( check != null ) check.invariant( vector ); if ( pre_check != null ) pre_check.insertElementAt( vector, obj, index ); // Save incoming values if needed for postcondition checking purposes VectorArrayImpl old_vector; Object old_obj; if ( post_check != null ) { old_vector = (VectorArrayImpl) vectore.clone(); old_obj = obj.clone(); } // Delegate to wrapped object vector.insertElementAt( obj, index ); // Checks after the method has completed if ( check != null ) check.invariant( vector ); if ( post_check != null ) post_check.insertElementAt( vector, old_vector, obj, old_obj, index ); } // ... more wrapped methods here ... } |
Figure 6. A VectorArrayImpl
wrapper.
The implementation of the wrapper’s insertElementAt()
method is divided into four sections. First, the wrapped vector’s
internal representation invariant and the method’s precondition are
checked by calling on the supporting helper objects. Note that calls
to these helper objects are guarded within if
statements.
This allows the wrapper to be constructed with null
references for one or more of the supporting helper objects,
effectively "disabling" the corresponding checks within the
wrapper.
Second, the wrapper saves the state of the wrapped object and any
method parameters that might be modified by the underlying operation.
This step is needed for full postcondition checking, since many
postconditions describe an object’s new state or a method’s return
value in terms of the "old" values at the time of method invocation.
This work is also guarded by an if
statement so that it
is only performed when postcondition checks are enabled.
Third, the wrapper invokes the underlying method on the wrapped object. Fourth, the wrapped object’s invariant is checked again and the corresponding postcondition is checked. Note that invariant checking always happens first, both in the first half of the operation before the wrapped object’s underlying method is invoked, and also in the second half after that method returns. If the invariant were not checked first--before either the precondition or the postcondition--then it is possible that the other checks could be performed on a wrapped vector in an (erroneously) inconsistent state, causing the checking code to crash. Proper ordering of the calls within the wrapper methods aids in preventing such problems wherever possible.
Figure 7 presents the
CheckListener
interface. A CheckListener
provides two versions of a basic notify()
method. Both
versions take identifying information about the kind of check as well
as the class and method in which checking is being performed. The
notify()
methods also take the Boolean result of the
check performed, indicating whether the check was successful or not.
This allows CheckListener
s that trace or monitor all
checking actions, rather than just failed checks. The second version
of notify()
also takes a message identifying the
condition being checked (a string version of the boolean expression
being tested, perhaps) for more informative presentation. While this
CheckListener
design is simple, additional enhancements
or features are easy to add.
public interface CheckListener { static final int PRECONDITION = 1; static final int POSTCONDITION = 2; static final int ABSTRACT_INVARIANT = 3; static final int CONCRETE_INVARIANT = 4; void notify( int type, String classname, String method, boolean result ); void notify( int type, String classname, String method, boolean result, String condition_msg ); } |
Figure 7. The notification interface for assertion failures.
The CheckListener
interface makes it easy to construct
classes that will respond to checks in different ways. It is simple
to construct a CheckListener
that throws a specific
exception intended to halt the current program or thread when a check
fails. Alternatively, messages about failed (or successful) checks
can be directed to a log file, presented in a separate GUI window, or
filtered and processed in any number of ways.
The precondition checking helper class for the example vector
implementation is shown in Figure 8. When
created, this helper object
receives a reference to the CheckListener
it will notify
for each check it performs. Other than the constructor, the
precondition checker exports one method corresponding to each method
in the Vector
interface. Since one precondition checking
object may be used to perform checks for many
VectorArrayImpl
objects, the VectorArrayImpl
being checked is passed in as an additional parameter for each
precondition check.
class VectorArrayImpl_PreChecks { protected CheckListener listener; protected VectorArrayImpl_PreChecks( CheckListener cl ) { listener = cl; } public void insertElementAt( VectorArrayImpl vector, Object obj, int index ) { listener.notify( Check_Listener.PRECONDITION, "Vector_Array_Impl", "insertElementAt", index >= 0 && index <= vector.elementCount ); } // ... more precondition checking methods here ... } |
Figure 8. A precondition checking class.
Figure 8 only shows the implementation of
one method in the precondition checking helper class. Other methods
are similar. The precondition check for
insertElementAt()
is relatively simple. There are no
requirements placed on the object being stored in the vector. The
only restriction is that the given index is between zero and the size
of the vector, including both end points.
Figure 9 presents the postcondition
checking helper class for the example vector implementation. As with
the precondition checker, this helper object receives a reference to a
CheckListener
via its constructor. The postcondition
checker also exports one method corresponding to each method in the
Vector
interface. For each incoming value that might be
modified by the operation being checked, the postcondition checker
takes in two values: one represents the "old" value on entry to
the operation, while the other represents the "new" value after the
operation has completed. This includes the component being operated
upon--in this case, the wrapped vector.
class VectorArrayImpl_PostChecks { protected CheckListener listener; protected VectorArrayImpl_PostChecks( CheckListener cl ) { listener = cl; } public void insertElementAt( VectorArrayImpl new_vector, VectorArrayImpl old_vector, Object new_obj, Object old_obj, int old_index ) { // First, check the simple conditions boolean result = new_vector.elementCount == old_vector.elementCount + 1 && new_obj == old_obj && new_vector.elementData[index] == old_obj; // Now check that the first segment of the vector is unchanged if ( result ) { for ( int i = 0; i < index; i++ ) { if ( new_vector.elementData[i] != old_vector.elementData[i] ) { result = false; break; } } } // Finally, check that the second segment of the vector is unchanged if ( result ) { for ( int i = index + 1; i < new_vector.elementCount; i++ ) { if ( new_vector.elementData[i] != old_vector.elementData[i - 1] ) { result = false; break; } } } listener.notify( Check_Listener.POSTCONDITION, "Vector_Array_Impl", "insertElementAt", result ); } // ... more postcondition checking methods here ... } |
Figure 9. A postcondition checking class.
The postcondition checker’s insertElementAt()
method
illustrates this convention. Both old (before insertion) and new
(after insertion) values for the vector are passed in, as are both old
and new values of the object being inserted. Because Java only
supports one parameter mode--pass by value--parameters of scalar data
types cannot be modified inside methods. As a result, only the old
(incoming) value of the index
is provided for checking.
However, since all objects are represented by reference, it is not
possible to tell from a method’s signature whether or not the method’s
implementation might modify such a parameter (even if inadvertently or
incorrectly). For Java, this means that old and new values for all
objects involved in a method call must be available to check a
postcondition properly. In other languages with richer parameter
passing modes, it may be possible to use parameter mode declarations
to determine when both old and new values must be available for proper
postcondition checking.
For the purposes of illustration, Figure 9
shows a complete check
of the postcondition for insertElementAt()
. In addition
to \checking the elementCount
and
capacityIncrement
data members, the postcondition checker
also checks every element within the valid region of the
elementData
array to ensure that the contents of the
vector were modified properly (and that other cells of the array were
not modified inappropriately). Such checking provides the best error
detection capabilities when violation detection wrappers are used
[Edwards00].
In practice, the component implementer may decide on the best tradeoff
between the cost of performing such checks and the degree of error
detection sought. Fortunately, with suitable modifications to the
factory scheme, the violation detection wrapper approach would even
support a component designer who wanted to provide both cheap (but
less complete) checks and complete (but more expensive) checks,
leaving the final decision up to the component integrator.
Implementing checks that correspond to the preconditions is all that is necessary to detect invalid uses of the component--where a client breaches the interface contract. One might assume that also checking the postconditions would be enough to detect errors in the component’s implementation. It is true that this is a useful step in increasing the visibility of defects, and many bugs can be detected this way. Checking postconditions alone, or even checking all specification-level conditions, does not guarantee that implementation-level defects will be spotted.
One can increase the defect-revealing capabilities of a violation checking wrapper by also checking implementation-level assertions [Edwards00], specifically representation invariants. An invariant property (or simply an invariant) for a class is some property that always holds true for newly created objects, and for any publicly exported method, if the property is true on entry it will also always be true upon completion of that method. There are two different varieties of class invariants: abstract invariants and representation invariants [Edwards97, Meyer97]. An abstract invariant captures properties of a class that are visible to the client; model-based specifications include such assertions. A representation invariant, on the other hand, captures properties of a class’ internal representation--its private (or protected) data members. Representation invariants are not usually present in formal interface specifications, and are instead annotated within a component’s implementation (if they are written down at all).
The invariant checking helper class follows a structure similar to
that of the other checking helpers.
Figure 10 shows the
VectorArrayImpl_InvChecker
. In this case, the checker is
testing the representation invariant, instead of an abstract,
client-visible invariant [Edwards97]. Since
no formal behavioral description
has been given for the Vector interface presented in
Figure 4, it is
not clear if the public contract includes an abstract invariant.
class VectorArrayImpl_InvCheck { protected CheckListener listener; protected VectorArrayImpl_InvCheck( CheckListener cl ) { listener = cl; } public void invariant( VectorArrayImpl vector, String method ) { listener.notify( Check_Listener.CONCRETE_INVARIANT, "Vector_Array_Impl", method, elementData != null && elementCount >= 0 && elementCount <= elementData.length && capacityIncrement >= 0 ); } } |
Figure 10. An invariant checking class.
In the case of the VectorArrayImpl
class, the representation invariant captures mutual constraints between the
data members: there must be an allocated array, the number of elements must be
able to fit within this array, and the number of elements must be
non-negative. Figure 10 also adds the
condition that the capacityIncrement
must be non-negative. This illustrates a
design choice made by the VectorArrayImpl
implementer. One can either require the
capacityIncrement to be non-negative (and then assume it is so everywhere), or
not require this property (and handle the case where it is not
everywhere). The example VectorArrayImpl
class here makes the
former choice, while Sun’s java.util.Vector
implementation makes the latter.
An additional design choice that should be considered is whether or
not the array slots at or past elementCount
must contain
null
values, or whether they may refer to
non-null
objects. Because Java provides garbage
collection, this design decision has relatively subtle consequences,
possibly delaying reclamation of unreachable objects. In other
languages, such a decision may be more important, since inconsistent
treatment may lead to storage leaks. For the purposes of this
example, the VectorArrayImpl
class does not require the
unused array slots to contain null
values.
Figure 11 presents an alternative
implementation of the invariant checking helper class. In this
version, rather than combining all of the invariant conditions into
one Boolean expression--and one CheckListener
notification--each clause of the invariant condition is separated into
a separate test. The alternate form of
CheckListener.notify()
is used so that a string
describing the condition can be provided along with the result of the
check. This version provides greater detail in its notification
behavior, at the expense of less run-time efficiency.
class VectorArrayImpl_InvCheck { protected CheckListener listener; protected VectorArrayImpl_InvCheck( CheckListener cl ) { listener = cl; } public void invariant( VectorArrayImpl vector, String method ) { listener.notify( Check_Listener.CONCRETE_INVARIANT, "Vector_Array_Impl", method, elementData != null, "elementData != null" ); listener.notify( Check_Listener.CONCRETE_INVARIANT, "Vector_Array_Impl", method, elementCount >= 0, "elementCount >= 0" ); listener.notify( Check_Listener.CONCRETE_INVARIANT, "Vector_Array_Impl", method, elementCount <= elementData.length, "elementCount <= elementData.length" ); listener.notify( Check_Listener.CONCRETE_INVARIANT, "Vector_Array_Impl", method, capacityIncrement >= 0, "capacityIncrement >= 0" ); } } |
Figure 11. An alternative invariant checking class.
The idea of using a factory [Gamma95] to isolate client code from decisions about which particular concrete class is used when creating new vector objects is standard practice for many object-oriented developers. There are many common approaches to implementing such factories, often in conjunction with the singleton pattern. Any reasonable factory approach is viable here, whether decisions about which concrete classes to use are controlled by program statements, registry properties, a separate initialization file, or some other means.
public abstract class VectorFactory { static private VectorFactory factory = null; protected VectorFactory() {} static public instance() { if ( factory == null ) { registerNewFactory( new VectorArrayImplFactory ); } return factory; } abstract public Vector newVector(); protected void registerNewFactory( VectorFactory vf ) { factory = vf; } } public class VectorArrayImplFactory extends VectorFactory { protected VectorArrayImplFactory() {} public Vector newVector() { return new VectorArrayImpl; } static public use() { registerNewFactory( new VectorArrayImplFactory ); } } public class CheckingVectorFactory extends VectorFactory { static private VectorArrayImpl_PreChecks pre_checks; static private VectorArrayImpl_PostChecks post_checks; static private VectorArrayImpl_InvCheck inv_check; protected CheckingVectorFactory( Check_Listener listener, boolean check_inv, boolean check_post) { listener = cl; pre_checks = new VectorArrayImpl_PreChecks( listener ); post_checks = check_post ? new VectorArrayImpl_PostChecks( listener ) : null; inv_check = check_inv ? new VectorArrayImpl_InvCheck( listener ) : null; } public Vector newVector() { return new VectorArrayImpl_Wrapper( pre_checks, post_checks, inv_check ); } static public use( Check_Listener cl, boolean post_check, boolean inv_check ) { registerNewFactory( new CheckingVectorFactory ); } } |
Figure 12. Factory support classes for creating Vector
s.
Figure 12 shows one possible factory
arrangement for creating vectors in the context of this example. The
VectorFactory
base class serves as a container for the
singleton vector factory used to create new vector objects. This base
provides the primary interface for client code that needs to create
vectors:
Vector my_vector =VectorFactory.instance().newVector();
Figure 12 also shows two concrete
subclasses of VectorFactory
. The
VectorArrayImplFactory
class creates "normal," unwrapped
VectorArrayImpl
objects. This is the "default" concrete
factory used in the absence of any other actions in this example,
although the component implementer has control over this choice.
The second concrete factory is the
CheckingVectorFactory
. As indicated by its name, this
concrete factory creates wrapped vector implementations that perform
run-time contract violation checking. The concrete factory subclasses
provide a static use()
method that can be called to
"replace" the singleton instance currently being used to create new vectors
with a new instance of the selected concrete factory. For example, to request unwrapped vectors in
a given program, the following statement could be used in the
main()
method:
VectorArrayImplFactory.use();
The static use()
method provided by the
CheckingVectorFactory
takes additional parameters to
control the behavior of checking wrappers. When this concrete factory
is selected, the client can choose which specific
CheckListener
will be used, and whether or not invariant
checking and postcondition checking are enabled or disabled. For
example, if RaiseCheckException
is a
CheckListener
implementation that prints a diagnostic
message and raises an exception when any check fails:
CheckingVectorFactory.use( new RaiseCheckException(), true, // enable postcond. checks true // enable invariant checks );
The violation detection wrapper strategy presented here provides a different approach to packaging and managing run-time assertions with a number of benefits. For programmers used to using conventional assertions, the wrapper approach also raises a number of questions. The wrapper approach presented here has been employed at earlier points during its evolution in the commercial development of a family of software products for over seven years [Hollingsworth00]. This collection of applications consists of approximately 250 components implemented in more than 100,000 lines of C++ code. The developers have cited the practice of using assertion-checking wrappers during unit, integration, and system testing as an important mechanism in achieving a high level of quality across all products in this application family. Despite such practical success, however, there are issues worthy of discussion in considering the wrapper-based approach.
The first critical observation about the approach is that it separates the assertion checks from the source code being checked. This is no surprise, since part of the motivation for the approach was the desire to separate checks from the underlying code. Unfortunately, this separation has negative consequences when the same information (how internal data members are managed, for example) is encoded in two different locations. Further, since many developers use assertions as a form of documentation as well as a run-time aid, removing assertions from the underlying code substantially reduces their utility in this direction.
The second critical observation is that the approach introduces extra development overhead to create and maintain the extra support classes needed. If practiced by hand, this strategy appears to provide only marginal benefits to the original component developer, while incurring at least some extra cost.
Both of these concerns are valid. Also, both are issues raised from the perspective of the original component developer--the only party to receive direct benefits from the presence of more conventional embedded assertions. The goal here, however, is to provide an effective way for the clients of reusable components (typically distributed in compiled form) to receive client-level benefits from run-time assertions, something they cannot easily obtain without having source code access under traditional approaches. Also, it is important to note that these concerns also focus on the textual location and management of assertions, rather than on the way checks are executed at run-time or the way they are distributed along with release versions of components to clients.
With careful planning, it is possible to achieve the best of both worlds: the benefits of behavioral contract assertions included in the original source code for locality of management and documentation, together with the wrapper-based deployment strategy. One approach is to adapt an existing behavioral contract framework, such as the Java Modeling Language (JML) [Leavens99]. JML allows one to use structured comments to provide a formal description of the behavior of a Java class directly embedded in the class source code. JML also provides the ability to automatically include run-time checking of such contract assertions (in most cases) in a manner similar to Eiffel. As has been noted before, this has the drawback of requiring source code in order to change decisions about the inclusion or exclusion of checks. The next research goal for this work is to extend JML to generate checking code in separate violation checking wrapper components. The majority of the wrapper support code discussed in Section 3 can be easily generated from the underlying component in a syntax-directed fashion--the only portion of the code requiring any creativity is the code in the checking helper classes that actually implements the assertions. Since JML already tackles the task of providing such checks (but in-line), transforming JML to place this code in separate wrapper components is a natural ideal. This provides a feasible solution to addressing the two biggest concerns about the wrapper strategy while still providing its benefits.
Another important issue with respect to assertion checking wrappers is the use of "internal" assertions within a method. The wrapper strategy only addresses client-visible, contract-level assertions rather than intermediate assertions about proper functioning within individual operations. However, developers are the primary users of such internal assertions, which are most helpful during development, testing, and later internal maintenance. Because such assertions do not necessarily have any relationship to the externally visible object state or to client-visible behavior, it is much less likely that they will be useful to the reusers of a fully-tested, released component implementation. As a result, it is likely that embedded assertions would still be used by the developer of a component before release, but be removed from released code, as is common practice. For similar reasons, assertions on non-public methods (usually helper methods) are not handled by wrappers. Since they are not visible to clients and are not part of the public contract, any interface violation detection scheme will not address them directly. Instead, assertions on such hidden, internal features are most appropriate during development, rather than after component release.
Another issue to consider is the fact that opening up access to the data members inside the wrapped class can be considered to violation encapsulation. For many developers, this is not a major issue. Elsewhere, other approaches to writing contract violation checks have been presented that address this concern [Edwards98]. Similarly, it is possible to write assertion checks in abstract, client-level terms rather than in the low-level terms of the wrapped component’s internal implementation [Edwards98], although a full discussion of this approach is outside the scope of this article.
Finally, it is worth considering whether to use aggregation or
inheritance to "wrap" checks around the methods of a class. Rather
than using a separate wrapper class containing a reference to a
VectorArrayImpl
, for example, why not just create a
subclass of VectorArrayImpl
that overrides all public
methods with implementations similar to those in
Figure 6? Why not go
further, and simply include the assertion checks directly in-line in
such a class, rather than place them in separate helper classes? When
only one concrete vector class is considered in isolation, these
alternatives appear equally feasible. However, consider a family of
concrete vector subclasses. In
this case, if one creates a "checking wrapper" as a subclass of one of
these, it becomes infeasible to share the checking code or the
wrapping code with other vector subclasses. Further, if checks are
included in-line in the wrapper class, it is difficult to extend the
checking code, since adding additional precondition checks after the
existing invariant and precondition checks (i.e., in the
middle of the method shown in
Figure 6) is not easy.
With the proposed violation checking wrapper class arrangement shown
in Figure 3, an entire tree of vector
subclasses can be handled by
creating a tree of each kind of checking helper class. Each checking
helper subclass would only have to add checks for the extra conditions
required by its corresponding vector implementation. With care, a
single wrapper could be used to manage the entire tree of vector
implementations.
The approach described here shares the same philosophy presented by Bertrand Meyer in describing design by contract [Meyer97]. Eiffel implements Meyer’s strategy for run-time checking of contracts. Eiffel’s assertion checking mechanism differs from the wrapper approach described here in several ways. First, assertions are embedded directly within the underlying component by the compiler, rather than being placed in a separate wrapper. Second, Eiffel provides a single Boolean expression slot for each assertion, which tends to discourage the inclusion of longer or more difficult to phrase constraints. Although one can always add new features to a class to implement such checks while keeping the assertions themselves small, this leads directly into serious disadvantages caused when potentially faulty methods on the wrapped object are used to check the health and conditions of that object.
JML [Leavens99, Leavens01] is similar to Eiffel in that assertions are directly embedded in component source and run-time checks of such assertions can be compiled into the corresponding component implementation. Unlike Eiffel, however, JML provides explicit separation between the abstract level of behavioral specification and the concrete details in a specific implementation. Assertions in JML can be phrased in abstract, client-level terms rather than in terms of implementation features.
Interestingly, Sun Microsystems has added an assert
facility to Java 2 Platform version 1.4
[JDK1.4]. This facility is
similar in spirit to assert
macros in C and C++: the
programmer can include an arbitrary Boolean expression in an assert
statement. By using command line options, the developer can instruct
the Java compiler to either insert code for checking assertions or to
ignore assertions. As an added twist, when assertion checking code
has been generated, it can be enabled or disabled through the Java
Virtual Machine, either on the command line or through run-time method
calls. This approach comes much closer to supporting assertions for
the component reuser, instead of just the original developer. The
approach described here allows per-object decisions about enabling
checks, however, and also provides a clear evolutionary path to
behavioral descriptions written in abstract terms rather than in
implementation terms. Further, Java’s assert
s are tied
into the language’s exception mechanism; raising an exception is the
sole notification action supported when an assertion fails. Sun’s
advice to developers reflects common practice, which fails to consider
the value of assertion checking to component reusers and integrators
in a commercial component marketplace: "Typically, assertion checking
is enabled during program development and testing and disabled for
deployment" [JDK1.4].
Sun’s approach is a significant advancement over the wide variety
of existing assertion utility classes available, most of which have
been inspired by C++ assert
macros. The Mozilla
project’s Assert
class is a solid, publicly available
example of this approach [Mozilla]. Such a
utility class normally provides
support for a standard response to failed assertions--raising a
specific exception, for example. Support is also usually provided for
enabling or disabling this action. Because Java does not provide for
conditional compilation, some overhead is introduced even when
assertions are disabled; completely removing this overhead
necessitates recompilation of the checked component’s source code, and
in many cases modification to that source code as well.
The assertion checking approach described by Rosenblum [Rosenblum95] using the Annotation Pre-Processor (APP) is also related. APP allows assertions to be embedded in C programs and then combines separately provided code for checking those assertions to produce a "checking" build. Assertions can be assigned to different levels of importance to provide a simple means of including or excluding selected groups of assertions. Unfortunately, APP is for procedural programming in C and does not directly address object-oriented concerns.
Finally, more recent work on AspectJ also allows checking code to be separated from both clients and components [Kiczales]. Using aspect-oriented programming, one would create a separate aspect containing checking code and then choose whether or not to weave this crosscutting decision into a non-checking implementation at build time. Because aspect-oriented programming nicely embodies the decision to interpose code adornments such as run-time contract checks in a separate layer between the client and the underlying component, in many ways it is philosophically close to the approach advocated here. Unfortunately, the build-time focus of AspectJ would typically require source code to work effectively and has not been extended to the tailoring of reusable components distributed in binary form.
The checking wrapper approach was initially described as part of a larger framework for run-time behavioral contract checking [Edwards98] that is also related to formal specification, specification-based testing, and parameterized programming. It is possible to extend the violation checking wrapper approach to serve as the cornerstone that enables a comprehensive specification-based testing strategy [Edwards01].
Violation checking wrappers are applicable anywhere defensive checks are appropriate in an object-oriented design. Checking wrappers facilitate unit testing, integration testing, and debugging. Their critical benefit is that they provide an easy way to manage and control the externally visible contract checking features of a software component without requiring one to have access to its source code. This strategy recognizes the benefits provided by contract checking, particularly when component-based development approaches using independently developed or commercially available software parts are used.
By separating run-time checks from the underlying implementation code and promoting them to the level of a separate class, the attention and concerns of the programmer are refocused. Instead of being concerned about cluttering the base component with checks or how to fit complex checks into simple Boolean tests, the programmer can concentrate on writing more thorough checks that stand on their own. Using the factory pattern makes it simple to insert or remove checks while all client code and base component code remains unchanged and requires no recompilation. This allows a natural transition from including full checks when a new component is acquired or a new combination of components is being integrated, to gradual removal of checks as component compositions become more trusted. Further, by splitting different groups of checks into separate classes, one can use the same techniques to manage preconditions or postconditions separately.
For these reasons, the wrapper-based approach to separating run-time checks provides the most important benefits of prior approaches while avoiding all of the earlier disadvantages. To obtain the advantages of using checks for documentation and keeping them "near" the implementation, it is possible to modify existing run-time check generation tools, such as JML, to deploy generated checks using wrappers. Further work is needed in this direction to obtain the best benefits from the wrapper approach together with the benefits of more traditional embedded assertion techniques.
This research is funded in part by NSF grant CCR-0113181. I also gratefully acknowledge the contributions to this work provided by Bruce W. Weide, Murali Sitaraman, and Joseph Hollingsworth, all of who have helped shape the violation checking wrapper approach. Thanks also go to Roy Tan and John Zaloudek, who helped perform some of the code development for this article.