C H A P T E R  8

Contract-Oriented Programming

by John Beresniewicz

This chapter will introduce you to a powerful software engineering paradigm called Design by Contract and a method for applying it to PL/SQL programming.

Design by Contract

The following quote struck me as a revelation of software-truth when I first read it sometime in the year 2000:

Design by Contract is a powerful metaphor that…makes it possible to design software systems of much higher reliability than ever before; the key is understanding that reliability problems (more commonly known as bugs) largely occur at module boundaries, and most often result from inconsistencies in both sides’ expectations. Design by Contract promotes a much more systematic approach to this issue by encouraging module designers to base communication with other modules on precisely defined statements of mutual obligations and benefits, not on vague hopes that everything will go right.

—Bertrand Meyer, Object Success

I had written quite a lot of PL/SQL code as part of developing an Oracle performance diagnostic tool for DBAs and had come to loathe chasing down runtime bugs, many of which were of precisely the kind noted by Meyer, which is to say misuse or confusion regarding APIs between modules. The promise of engineering such bugs out of my code motivated me to learn more about Design by Contract and apply it somehow to PL/SQL programming.

Software Contracts

Design by Contract makes the observation that software modules have client-supplier relationships that can be modeled after legal contracts where two parties enter into an agreement of mutual self-interest and obligation. Each party expects to benefit somehow from the contract, and each party is usually also under some obligation from the contract. In the world of software, contracts can be thought of as the rules under which the APIs between calling and called modules must function. The calling module provides some input values or other system state when the API is invoked, and the called module is expected to reliably compute some output or resultant system state upon completion. If the rules governing these inputs and outputs are broken, there is a contract violation and the software has a defect, or bug.

The concept of software contracts governing APIs is powerful for many reasons, but most especially because a high percentage of bugs are due to confusion or misuse at API boundaries, as suggested in the quote from Meyer. If we can enforce API contracts in some way such that contract violations are exposed immediately, we can discover an entire class of bugs quickly and improve the reliability of software greatly.

Basic Contract Elements

Three basic formal elements are used to define the terms of software contracts: preconditions, postconditions, and invariants. These abstract contract elements are documented and enforced in the code by using software mechanisms typically called assertions.

Preconditions

Preconditions are conditions or states that must be true in order for a module to compute correct results. They represent obligations on the callers of a module under the contract and accrue benefits to the module itself. Preconditions benefit the module because they represent hard facts that the module’s algorithms can rely on. Preconditions oblige callers because it is the responsibility of callers to make sure the preconditions are met prior to calling the module.

It is a contract violation to call the module while failing to satisfy the preconditions; therefore precondition violations indicate bugs in the calling code. The module is not even under any obligation to compute a result in this case, as the “terms of the deal” have not been met.

At the code level, preconditions are ideally checked prior to module entry by the execution environment itself. Since PL/SQL does not offer native support for contract elements, it is recommended that all modules enforce their preconditions immediately upon module entry, especially those that govern the validity of input variable values.

Postconditions

Postconditions specify conditions on the output or computed results that the module guarantees will be true when it completes its computation, provided the preconditions have been met. They represent a fundamental obligation on the module to compute correct results. Postconditions are a benefit to the callers of the module precisely in that they allow callers to trust the module’s output.

Failure to satisfy postconditions is a contract violation by the module itself, and indicates defects are present in the module. As a practical matter at the code level, it can be very difficult to assert the full correctness of a module’s outputs at runtime either from within or without the module, as doing so can imply having independent implementations of the module’s computational requirements to compare against each other for equality.

However, it may often be the case that limited or partial postconditions can be imposed on the module’s outputs that are sufficient to indicate a contract violation. For instance, if a function is intended to compute some result that should always be a positive number, then it will be a contract violation for it to return a negative number, and this can be imposed as a postcondition on the module.

Unit and regression testing can be thought of as validating postconditions under known preconditions and input values.

Invariants

Invariants specify states or conditions that should always hold true. For instance, a PL/SQL package may make use of private shared data structures accessed and perhaps modified by the package’s modules. There may be various consistency or integrity requirements over these data structures that can be defined and considered shared contract elements imposed on the entire package itself.

A module that completes execution and causes the invariants to become invalid has a bug. Invariants can be difficult to identify and express programmatically and are often expensive to enforce.

Assertions

Assertions are the basic mechanism for expressing and enforcing software contract elements in the code itself. Assertions basically encapsulate statements about system state that must be true or there is an error. That is, when an assertion tests false, the system is known to be in error; this should be communicated out, usually in the form of an exception.

Technically, preconditions, postconditions, and invariants are all different kinds of assertions as they capture different contract elements. In applying Design by Contract to PL/SQL programming, I recommend building a simple and consistent assertion mechanism and using that mechanism to express and enforce preconditions, postconditions, and invariants (where possible.)

Assertions are essentially correctness tests built directly into the software that find and signal bugs at runtime and as such are a powerful means of ensuring software reliability.

References

You can find much more information about the principles and practice of Design by Contract in the following books:

  • Object-Oriented Software Construction, Second Edition by Bertrand Meyer (Prentice Hall PTR, 1997)
  • The Pragmatic Programmer by Andrew Hunt and Dave Thomas (Addison Wesley Longman, Inc., 2000)
  • Design by Contract by Example by Richard Mitchell and Jim McKim (Addison-Wesley, 2002)
  • Design Patterns and Contracts by Jean-Marc Jezequel, Michel Train, Christine Mingins (Addison Wesley Longman, 2000)

Implementing PL/SQL Contracts

With the conceptual framework for Design by Contract in place, the next step is to design and implement a PL/SQL assertion mechanism (or mechanisms) with which to enforce software contracts between PL/SQL programs.

Basic ASSERT Procedure

A very simple assertion mechanism can be implemented in PL/SQL as a procedure that accepts a Boolean input argument and either exits silently when it tests TRUE or complains loudly by raising an exception when it tests FALSE. The simple procedure in Listing 8-1 implements exactly this logic.

Listing 8-1. Basic Assert Procedure

PROCEDURE assert (condition_in IN BOOLEAN)
IS
BEGIN
   IF NOT NVL(condition_in,FALSE)
   THEN
      RAISE_APPLICATION_ERROR(-20999,'ASSERT FAILURE'),
   END IF;
END assert;

As simple as it is, there are a couple of points to note about this procedure. First and very importantly, the procedure takes the position that a NULL input value will test as FALSE and raise the exception. This means that the procedure expects an actual value to be passed in or the assertion itself is invalid—in other words, the procedure has been improperly called. In contract terms, the procedure has a precondition that the input parameter (condition_in) is NOT NULL.

Another thing to note is that while the procedure provides a message indicating the exception signals an assertion failure, this message does not communicate anything about the failing condition itself. Since assertion failures always indicate bugs, it would be useful to have additional information specific to the condition that failed for debugging purposes.

The assertion procedure can be improved by adding parameters to supply contextual information along with the Boolean condition for use in creating a more informative error message to return with the ASSERTFAIL exception. Listing 8-2 provides an example.

Listing 8-2. Improved Basic Assert Implemented as a Public Procedure

CREATE OR REPLACE
PROCEDURE assert (condition_IN IN BOOLEAN
                 ,msg_IN  IN VARCHAR2 := null
                 ,module_IN IN VARCHAR2 := null)
IS
  ASSERTFAIL_C CONSTANT INTEGER := -20999;
  BEGIN
    IF NOT NVL(condition_IN,FALSE) -- assertfail on null
    THEN
      RAISE_APPLICATION_ERROR      -- per doc accepts 2048
              ( ASSERTFAIL_C, 'ASSERTFAIL:'||SUBSTR(module_IN,1,30)||':'||
                SUBSTR (msg_IN,1,2046) ); -- fill 2048 with msg
END IF;
END assert;
/
CREATE OR REPLACE PUBLIC SYNONYM assert FOR assert;
GRANT EXECUTE ON assert TO PUBLIC;

Here the assert procedure is made globally available via public synonym and execute privileges. In this example, the ASSERTFAIL error number is provided to RAISE_APPLICATION_ERROR via reference to a local constant (ASSERTFAIL_C) in the example but that could also reference a package-level constant. Using the constant avoids hardcoding a literal number in the call to RAISE_APPLICATION_ERROR and affords better maintainability should there be a need to change its value.

The signature of this assert includes two new parameters: module_IN and msg_IN. The module_IN parameter allows the assertion-calling code to identify its source module. The second parameter, msg_IN, permits the calling code to provide some diagnostic detail about the contract element.

When the condition_IN parameter tests FALSE, there is a contract violation and ASSERTFAIL exception is raised. The actual message returned with an ASSERTFAIL exception is constructed as a colon-delimited composite of the string “ASSERTFAIL”, the program name (module_IN), and finally a message passed in the call to assert (msg_IN.) In this way, the ASSERTFAIL exception can provide very specific information about location and cause of the contract violation, greatly assisting bug isolation and diagnosis.

I can execute a simple test of the procedure in SQL*Plus as follows:

SQL> l
  1  declare
  2  procedure my_proc(i number)
  3  is
  4  begin
  5    assert(i>3,'i>3','my_proc'),
  6  end;
  7  begin
  8  my_proc(1);
  9* end;
SQL> /
declare
*
ERROR at line 1:
ORA-20999: ASSERTFAIL:my_proc:i>3
ORA-06512: at "APPL01.ASSERT", line 9
ORA-06512: at line 5
ORA-06512: at line 8

Note how the exception message tells us where and why the ASSERTFAIL was raised: the condition that the parameter i must be greater than 3 failed in module my_proc. This is incredibly useful; the ASSERTFAIL indicates a bug in the code and the exception message helps isolate and triage the bug. The call stack indicates the initiating line of code is number 8 of the anonymous PL/SQL block, which calls my_proc, which calls APPL01.ASSERT at its line number 5 to assert the contract condition on the input parameter (namely i>3). The specific module and failing condition are exposed in the ASSERTFAIL error message by making good use of the module_IN and msg_IN parameters of assert. If the assertion is enforcing a precondition, it will be important to diagnose the caller of the module failing the assertion, as precondition bugs implicate the calling code.

Standard Package-Local ASSERT

Most substantive programming in PL/SQL takes place inside packages, so deciding how to enforce contracts within the context of a package is an important consideration. I developed simple, standardized package-local assertion implementation to use in all my PL/SQL packages. Using only this relatively simple construct and associated coding discipline, significant progress can be made in defining and enforcing PL/SQL software contracts rigidly.

The first thing to standardize relative to contract enforcement is the mechanism and meaning for signaling contract violations. The mechanism adopted is to raise a specific exception (called ASSERTFAIL) using the RAISE_APPLICATION_ERROR built-in procedure. The specification for package DBC_example in Listing 8-3 illustrates declaring the ASSERTFAIL exception and associating it to a specific exception number using the PRAGMA compiler directive.

Listing 8-3. Standardized Package Specification Template

CREATE OR REPLACE PACKAGE DBC_example
AS
  ----------------------------------------------------------
  -- SLPA declarations (standard local packaged assertion)
  ----------------------------------------------------------
  ASSERTFAIL     EXCEPTION;
  ASSERTFAIL_C   CONSTANT INTEGER := -20999;
  PRAGMA EXCEPTION_INIT(ASSERTFAIL, -20999);
  ----------------------------------------------------------
END DBC_example;
/

All packages within an application scope should standardize on a single error number to use for the ASSERTFAIL exception to avoid confusion. In this way, ASSERTFAIL obtains a common meaning across the entire application. This is important, as clear contract enforcement requires unambiguous and common interpretation of contract violations, the whole purpose of ASSERTFAIL. Finally, the exception number for ASSERTFAIL (-20999) is also assigned to a package constant for referencing inside the assert procedure itself. Similarly, begin all packages with a standardized implementation as shown in Listing 8-4, taken from the body for the DBC_example package.

Listing 8-4. Standardized Package Body Example

CREATE OR REPLACE PACKAGE BODY DBC_example
AS
  -- package name for assertfail msg
  PKGNAME_C      CONSTANT VARCHAR2(20) := $$PLSQL_UNIT;  -- NOTE 10g construct  
  -- foreward declare assert so all module implementations can reference it
  PROCEDURE assert (condition_IN IN BOOLEAN
                   ,msg_IN       IN VARCHAR2 := NULL
                   ,module_IN    IN VARCHAR2 := NULL);
  ----------------------------------------------------------
  -- standard local packaged assertion procedure
  ----------------------------------------------------------
  PROCEDURE assert (condition_IN IN BOOLEAN
                   ,msg_IN       IN VARCHAR2 := NULL
                   ,module_IN    IN VARCHAR2 := NULL)
IS
    l_assertmsg VARCHAR2(2048) := 'ASSERTFAIL:'||SUBSTR(PKGNAME_C,1,30)||'.'; -- assertmsg
  BEGIN
    -- test the asserted condition
    IF NOT NVL(condition_IN,FALSE)   -- fail on null input
    THEN
      -- finish initializing assertmsg
      l_assertmsg := l_assertmsg || SUBSTR(NVL(module_IN,'?MODULE?')||':'||msg_IN,1,2046);
      -- raise the standardized exception
      RAISE_APPLICATION_ERROR (ASSERTFAIL_C, l_assertmsg, FALSE); -- FALSE
    END IF;
  END assert;
  
BEGIN
  assert(TRUE); -- pkg initilization token
END DBC_example;
/

The assert in Listing 8-4 closely resembles the one presented earlier as a public procedure in Listing 8-2 with the slight change that it uses the Oracle 10g inquiry directive $$PLSQL_UNIT to identify the package rather than rely on a separately declared (and potentially erroneously initialized) constant. The call to RAISE_APPLICATION_ERROR will trim a long message to just fit into 2048 characters, allowing up to 30 characters for the package name. In practice, messages that challenge this limit would not necessarily be so helpful but the use of substrings helps correctness and robustness of the code.

Listing 8-4 does not represent much code penalty to pay for having a local assert available in all packages independent of other procedures. I prefer the idea of packages that can compile completely independently, at least at inception (and even somewhat resent RAISE_APPLICATION_ERROR for this reason.)

So my personal preference is to begin all PL/SQL packages using standard specification and body templates like these, such that standardized usages for contract enforcement can be adopted within and across packages.

Alternatively, you could adopt a simple, standardized PUBLIC assert procedure as in Listing 8-2 and have all contract enforcement depend on it.

Finally, I could suggest you create a really slick shared package implementation of assert with fancy call stack processing to assist bug diagnosis, but will not recommend it. The purpose of this chapter is to educate and convince you to enforce contracts with simplicity in your code, not to impress with fancy assertion mechanisms.

LOCAL OR GLOBAL ASSERT?

Enforcing Contracts Using ASSERT

Having decided to standardize on an assertion mechanism and embed it in every package, the question now becomes how to put it into use enforcing contracts? For this a little discipline is required; however, in my experience, the extra investment in coding up front is far more than paid back by the assurance that an entire class of important bugs is either not present or will be immediately detected by the code itself.

The procedure in Listing 8-5 illustrates using the standardized assert to enforce a good singular: precondition and silly postcondition directly in code. The precondition is that the single input parameter be not null, which is not declaratively enforceable and is often a very important requirement to demand, as discussed later. The postcondition illustrates by contrast the value of adopting calling discipline when using assert in code. The assertion fails to make use of module_IN or msg_IN parameters and thus provides little to go on relative to debugging the issue.

Listing 8-5. An Example Procedure Making Good and Bad Use of Assert

PACKAGE BODY DBC_example

  ----------------------------------------------------------
  -- test/example procedures and functions
  ----------------------------------------------------------
  PROCEDURE proc01 (p1 varchar2)
  IS
    --  local module name for assert calls
l_module VARCHAR2(30) := 'PROC01';
  BEGIN
    -- enforce NOT NULL inputs precondition
    assert(p1 IS NOT NULL,'p1 NOT NULL', l_module);

    -- procedure logic
    null;

    -- a very bad postcondition!
    assert(FALSE,'Boo hoo on you'),  -- assertfail w/out passing module
  END proc01;

Proc01 takes a single input parameter p1 and asserts the contract precondition that it be NOT NULL using this line of code:

assert(p1 IS NOT NULL,'p1 NOT NULL',l_module);

Note that this precondition is tested in the very first line of the module’s main code block. Remember that under software contracts modules are not obliged to compute outputs if preconditions are not met, so testing them immediately allows modules to fail early and not waste compute cycles unnecessarily.

Also note the conventions adopted for passing extra information to assert through the parameters msg_IN and module_IN. In the first case, the message is simply a string literal of the Boolean expression constituting the contract condition to be asserted. In the second, the module declares a local variable l_module and initializes it with its own name, which is also passed to assert.

In addition to asserting the contract precondition against NULL inputs, proc01 implements some illustrative buggy logic: it asserts a final postcondition that will always throw the ASSERTFAIL exception but without passing any useful diagnostic information to include in the error message.

Testing proc01 by calling it using a NULL input value spools the following results from SQL*Plus:

BEGIN DBC_example.proc01(null); END;

*
ERROR at line 1:
ORA-20999: ASSERTFAIL:DBC_EXAMPLE.PROC01:p1 NOT NULL
ORA-06512: at "APPL01.DBC_EXAMPLE", line 104
ORA-06512: at "APPL01.DBC_EXAMPLE", line 25
ORA-06512: at line 1

At first glance, these results may not seem all that impressive, but to me they are pure gold. For example, the first ORA-20999 error indicates the following:

  • There is a contract violation bug in the code, indicated by the ASSERTFAIL.
  • The bug was caught in module PROC01 of the DBC_EXAMPLE package.
  • The contract violation was that input variable p1 failed the NOT NULL check.
  • Call stack indicates the assertion check in line 25 of the DBC_EXAMPLE package failed.

A bug has been identified for certain, the code location where bug was caught is known, and the nature of the bug is also revealed. Think for a minute about the last time you tried to chase down a bug involving unexpected run-time data values, and then tell me this is not totally cool.

Reading the call stack is not intuitive and requires some knowledge of the implementation. Beginning with the ORA-20999 error, which is the call to RAISE_APPLICATION_ERROR inside the local assert, the next in the stack must be the assert procedure itself, so the third entry on stack is the code location that called assert. This is important in that the next entry on the stack is the caller of the module making the call to assert. When the assertion is a precondition then this caller violated the contract condition, so the calling code has a bug exposed at the line indicated (line 1 in the trivial example here.) Executing a second test of proc01 that passes the non-NULL input precondition but trips on the bad assertion gives the following output:

BEGIN DBC_example.proc01('VALUE'), END;

*
ERROR at line 1:
ORA-20999: ASSERTFAIL:DBC_EXAMPLE.?MODULE?:Boo hoo on you
ORA-06512: at "APPL01.DBC_EXAMPLE", line 104
ORA-06512: at "APPL01.DBC_EXAMPLE", line 29
ORA-06512: at line 1

This second ASSERTFAIL helps illustrate the value of adopting the extra discipline required to pass in informative values for the module name and message parameters to assert. In this case, it is still known there is a contract violation bug and in which package the bug was caught, but the lack of additional diagnostic information is clearly disadvantageous.

My strong recommendation is that you take the time to always include both the module name and an informative description of the failing condition in your calls to assert. Many modules may have multiple input parameters, and each of them may be subject to one or even more than one contract preconditions. It will seem like a lot of extra typing to do without furthering the computational goals of the module. Believe me, the first time an ASSERTFAIL exception jumps out at runtime and informs you exactly the where and what of a previously undiscovered bug, you will be extremely glad you invested the effort.

An Additional Improvement

As noted already, precondition violations indicate bugs in calling modules while postcondition violations are bugs in the called module. Therefore, it is vitally important for assigning responsibility for contract violation bugs to know whether an ASSERTFAIL issued from a precondition or postcondition assertion.

The improvement in Listing 8-6 was suggested by my friend Steven Feuerstein as a means of distinguishing between pre- and postcondition violations.

Listing 8-6. Distinguishing between Pre- and Postconditions

-- assert for preconditions
  PROCEDURE assertpre (condition_IN IN BOOLEAN
                       ,msg_IN      IN VARCHAR2 := NULL
                       ,module_IN    IN VARCHAR2 := NULL)
  IS
  BEGIN
     assert (condition_IN,'Pre:'||msg_IN,module_IN);
  END assertpre;
-- assert for postconditions
  PROCEDURE assertpost(condition_IN IN BOOLEAN
                       ,msg_IN       IN VARCHAR2 := NULL
                       ,module_IN    IN VARCHAR2 := NULL)
  IS
  BEGIN
     assert (condition_IN,'Post:'||msg_IN,module_IN);
  END assertpost;

The idea is simply to have separately named procedures to assert preconditions and postconditions that call the original assert but add either “Pre:” or “Post:” to the message. These extra routines do not add much code baggage and significantly improve bug diagnostics, so I have added them to the standardized package template. Of course, they must be called in the right context to avoid confusion: never call assertpost to check preconditions nor assertpre for postconditions.

Contract-Oriented Function Prototype

PL/SQL functions offer additional opportunity to impose more contract discipline into code. Consider the function in Listing 8-7 as a prototype for all PL/SQL functions.

Listing 8-7. Prototype PL/SQL Function

FUNCTION  func01 (p1 varchar2) RETURN NUMBER
  IS
    l_return NUMBER;
    l_module VARCHAR2(30) := 'FUNC01';
  BEGIN
    -- preconditions
    assertpre(p1 IS NOT NULL,'p1 NOT NULL',l_module);

    -- function logic
    null;

    -- postcondition check and return
    assertpost(l_return IS NOT NULL, 'RETURN NOT NULL',l_module);

    RETURN l_return;
  END func01;

Note the following key points about this function:

  • It returns results through a local variable declared with a standardized name (and proper type of course).
  • It asserts preconditions using module identifier and message conventions at the beginning.
  • It exits using RETURN in exactly one place: the last line of code.
  • It asserts postconditions using standard conventions immediately preceding the RETURN statement.

Since functions will always have a single return value, they can and should be coded such that there is a single exit point at which this value is returned to the calling module. Following this convention allows postcondition checks to be specified in exactly one place, immediately preceding the RETURN statement.

The use of a local variable with a standard name to always use for the RETURN statement allows me to know whenever the return value is being manipulated by the function logic. Clever functions with complex expressions computing outputs directly in the RETURN statement may seem elegant but they compromise the level of control over postcondition assertions that following my simple convention provides.

Adhering to the function prototype’s coding conventions greatly benefits code reliability, as functions can enforce contract terms at well-defined entry and exit points. Functions (or procedures) with multiple possible exit points may introduce the need for postcondition assertions to be duplicated, which can itself be a source of bugs should contract terms need to be altered.

Example: Testing Odd and Even Integers

The examples so far have been trivial showcases for calling precondition and postcondition assertions without any real computational requirements. This section will present a pair of functions with actual computational requirements, albeit very modest and perhaps not very practical ones.

Imagine an application with logic that depends critically on whether specific data values are odd or even integers. In such an application, it may be useful to have modules that conform to the following contractual agreements:

  • A function that accepts a non-NULL integer as input and returns the Boolean TRUE if the integer is an odd integer and FALSE otherwise.
  • A function that similarly accepts a non-NULL integer and returns TRUE when the input integer is even and FALSE otherwise.

These two functions are clearly related in that for any given integer they should return exactly opposite results since the integer can only be either even or odd and must be one of these two. The code in Listing 8-8 uses the function prototype and coding conventions presented earlier to meet the first requirement.

Listing 8-8. A Function to Test a Number for Oddity

FUNCTION  odd (p1 number) RETURN BOOLEAN
  IS
    l_return BOOLEAN;
    l_module VARCHAR2(30) := 'ODD';
  BEGIN
    -- preconditions
    assertpre(p1 IS NOT NULL,'p1 NOT NULL',l_module);
    assertpre(p1 = TRUNC(p1), 'p1 INTEGER',l_module);

    -- function logic: returns TRUE if p1 ODD
    l_return := MOD(p1,2)=1;
-- postcondition check and return
    assertpost(l_return IS NOT NULL, 'RETURN NOT NULL',l_module);

    RETURN l_return;
  END odd;

Listing 8-8’s function is almost identical to the function prototype in Listing 8-7. The function in Listing 8-8 asserts preconditions and postconditions at the beginning and end of the function respectively, returning the computational result in the final line of code.

The function asserts two preconditions: that the input is non-NULL and additionally that it is an integer. Clever readers may observe that the second assertion implies the first, in the sense that if the input value for p1 is NULL then this second assertion will fail as well and the first can be considered redundant in some sense. It may be tempting in such instances to only assert the second condition; however, this represents a shortcut that obfuscates the fact that there really are two separate preconditions: that the input be non-NULL and that it be an integer-valued number. Part of the motivation for adopting a contract-oriented approach to coding is the desire to be very explicit about contract requirements in the code itself, so compound assertions should be preceded by any simpler assertions on their components, even if these are implied by the compound expression. This illustrates an important added benefit of assertions in code: they document as well as enforce software contracts.

The computational requirements of the odd function are met in a single line of code, yet the function itself contains five lines of executable code. A minimal implementation of our functional requirements could be met by the function in Listing 8-9.

Listing 8-9. Minimal Implementation of the Test for Oddity

FUNCTION  odd2 (p1 number) RETURN BOOLEAN
  IS
  BEGIN
    RETURN MOD(p1,2)=1;
  END odd2;

While the implementation in Listing 8-9 meets the basic computational requirement to return TRUE when p1 is an odd integer, it does not enforce the contractual requirements to accept only non-NULL integers and return only TRUE or FALSE. If a NULL value for p1 is passed, the function will return NULL as output even though it purports to be a Boolean. This obliges calling modules to either assert non-NULL on input values themselves or to accommodate NULL outputs using NVL or other special-case constructs. So, the extra code required to ensure correctness has not disappeared, rather it is moved into the calling applications, all of which must get it right.

Similarly, the minimal implementation does not check that p1 is an integer, and the MOD function will happily return results for non-integer values of its arguments so the function will not fail in this case; however, it is very unlikely that a system that depends on oddness or evenness of integers will be satisfied with routines that accept non-integer inputs and makes statements about whether they are odd or not.

Now consider the implementation in Listing 8-10 of the complementary function even that computes whether an input integer is an even number.

Listing 8-10. A Function to Test for Evenness

FUNCTION  even (p1 number) RETURN BOOLEAN
  IS
    l_return BOOLEAN;
l_module VARCHAR2(30) := 'EVEN';
  BEGIN
    -- preconditions
    assertpre(p1 IS NOT NULL,'p1 NOT NULL',l_module);
    assertpre(p1 = TRUNC(p1), 'p1 INTEGER',l_module);

    -- function logic: returns TRUE if p1 EVEN
    l_return := p1/2 = TRUNC(p1/2);

    -- postcondition check and return
    assertpost(l_return IS NOT NULL, 'RETURN NOT NULL',l_module);
    assertpost(NOT(l_return AND odd(p1)),'NOT(EVEN AND ODD)',l_module);
    assertpost(l_return OR odd(p1),'EVEN OR ODD',l_module);

    RETURN l_return;
  END even;

Listing 8-10’s function looks very similar to the odd function (Listing 8-8) in that it closely follows the contract-oriented function prototype, yet there are some important differences to note. For one thing, the function logic could easily have used modulo logic just as the odd function did by using l_return := MOD(p1,2)=0 but instead uses simple division by 2 and the TRUNC function:

l_return := p1/2 = TRUNC(p1/2);

This introduces the most interesting feature to note about even, which is the extensive assertion of postconditions. First the simple expectation that l_return is non-NULL is asserted, followed by these logic puzzles:

assertpost(NOT(l_return AND odd(p1)),'NOT(EVEN AND ODD)',l_module);
assertpost(l_return OR odd(p1),'EVEN OR ODD',l_module);

These two complex assertions actually test the correctness of the value about to be returned by even using the complementary odd function. The first asserts that the two functions even and odd cannot both agree about p1, if one is FALSE the other is TRUE and vice versa. The second asserts that at least one of them must be FALSE for any given p1.

The assertions test the correctness of the function results by checking them against results of a computationally independent function with a different algorithmic strategy. This is one of those rare cases when the correctness of computational results can actually be asserted as a postcondition.

Of course, nothing comes entirely for free and in this case asserting the complex postcondition really amounts to performing the computational logic three times - once in the even function itself and twice again in calling the odd function in postcondition assertions. Tight coupling with the odd function also means that if there is a bug in that function it will extend as a bug into the even function as well.

Useful Contract Patterns

There are several useful contract patterns that I have come to recognize and appreciate in my own code, described in the following subsections. Adopting well-understood and agreed patterns like these for modules promotes code correctness and clarity in that certain contracts are understood to apply based on the pattern and do not require module-by-module specification or analysis.

Not-NULL IN / Not-NULL OUT

Perhaps the most basic and useful software contracts in a PL/SQL context are those that eliminate the problematic and often ambiguous circumstances that surround the handling of NULL values. Most algorithms expect to compute over actual data and may need to special case or provide alternate computational results in the presence of missing or NULL data values. This can introduce unnecessary ambiguity or complexity into APIs, much better to simply outlaw NULL values wherever possible.

Figuring out bugs due to NULL values can be very difficult, especially when computations do not explicitly fail as a result. Computer program logic is by its very nature binary, and NULL is an unwelcome third wheel.

So one of my favorite contracts can be simply described (from the called module’s point of view) as follows: if you agree NOT to pass me NULL input values, you will certainly receive non-NULL output values back. This contract agreement is almost universally valuable whenever it can be imposed. In practice, a large majority of contract enforcement is validating input preconditions and this often means accepting only non-NULL values. Of course, the module must then also adhere to and enforce wherever possible its obligation under the postcondition that only non-NULL outputs will be generated. There is also an inherent fairness in this contract pattern: both parties are held to the same obligation and accrue similar benefits of certainty.

FUNCTION RETURN Not-NULL

This pattern is a specialization of the previous one that recognizes the significant advantages of using functions instead of procedures when a module must return output to callers. The function-based programming model lends itself to disciplined contract enforcement as illustrated in the function prototype discussed previously. In addition, functions that reliably return non-NULL values can be referenced in calling code in place of constants or variables. For instance, imagine a function called my_function that takes a single input parameter p1 and guarantees to return a non-NULL number as output. This allows me to safely write code like the following:

IF my_function(p1=>'X') > my_function(p1=>'Y')
THEN do_something;
END IF;

The intent here is clear, unambiguous, and reads directly from the code: compare my_function results over the parameter values ‘X’ and ‘Y’ and take action if the former is greater than the latter.

Now suppose that my_function does not guarantee non-NULL output values. Now any code that calls it must itself make decisions about what to do in that case, for example:

IF NVL(my_function(p1=>'X'), 0) > NVL(my_function(p1=>'Y'), 1)
THEN do_something;
END IF;h

This code is much more difficult to read due to the distracting syntax clutter introduced by the pair of NVL function calls. In addition, the intent is far less obvious, especially given that NULL output from my_function is cast to 0 when the input value is ‘X’ and to 1 when the input value is ‘Y’. Why is this the case? Is it a bug in this code or some mysterious consequence of the inner purpose of my_function? This is what happens when function contracts are weak and callers must adapt to ambiguous outputs.

FUNCTION RETURN BOOLEAN Not-NULL

A specific case of the non-NULL function pattern occurs when the function’s RETURN datatype is BOOLEAN. Such functions can safely be referenced in conditional statements to implement powerful and expressive logic.

For instance, suppose there is a procedure to perform some complex processing on records of some type my_rectype, but these must pass some sophisticated criteria in order to be successfully processed. These criteria may actually be preconditions to the processing procedure, or perhaps they are additional criteria that are not required by that procedure but which the program logic wishes to impose. These criteria can be encapsulated in a function whose contract is to test these criteria and unambiguously return TRUE or FALSE.

FUNCTION ready_to_process (rec1 my_rectype) RETURN BOOLEAN;

The code that performs processing of records might look like this:

IF ready_to_process(rec1=>local_rec)
THEN
  process_record(rec1=>local_rec);
ELSE
  log_error;
END IF;

Again, not having to deal with NULL function output combined with expressive function names makes the intent of such code clear.

Check Functions: RETURN TRUE OR ASSERTFAIL

The previous example leads to an interesting consideration regarding asserting the truth-value of functions that return BOOLEAN. For instance, should the process_record procedure assert the ready_to_process function as a precondition?

PROCEDURE process_record (rec1 my_rectype)
IS
    l_module VARCHAR2(30) := 'PROCESS_RECORD';
BEGIN
  -- precondition: test that record ready to process
  assertpre(ready_to_process(rec1), 'REC1 ready to process', l_module);

The answer as to whether this is OK or not is, it depends. Bertrand Meyer has this to say about it:

If you exert the proper care by sticking to functions that are simple and self-evidently correct, the use of function routines in assertions can provide you with a powerful means of abstraction.

Bertrand Meyer, Object-Oriented Software Construction, Second Edition

I love the phrase “simple and self-evidently correct” as it succinctly captures the essence of bug-free code. In my own programming, I have sometimes found that multiple modules within the same package may all reference and manipulate some private global data structures and all need to assert a common set of preconditions over these structures to guarantee correctness. In this case, implementing a function that combines these assertions together can eliminate significant code duplication.

FUNCTION check_all_ok(module_IN VARCHAR2) RETURN BOOLEAN
IS
BEGIN
  assert(condition1, 'condition1', module_IN);
  assert(condition2, 'condition2', module_IN);
  …
  assert(conditionN, 'conditionN', module_IN);

  RETURN TRUE;
END check_all_ok;

As long as the conditions tested are themselves simple expressions limited to package variables or data structures and PL/SQL language primitives, such “check functions,” can be considered “simple and self-evidently correct” in that they can clearly only return TRUE or throw an ASSERTFAIL exception, the check_all_ok function can safely be asserted in the various modules that depend on it.

assertpre(check_all_ok(l_module), 'check_all_ok', l_module);

Note that the local module name of the caller is passed into the check_all_ok function so that any ASSERTFAIL exceptions generated will be correctly tagged with the module name in which the check failed.

Principles For Bug-Free Code

This chapter has actually covered a considerable amount of material rapidly and with brevity. The references cited earlier near the beginning of the chapter can give you a much fuller treatment of the depth and richness of Design by Contract. I mentioned my attraction to contract-oriented programming is rooted in an abhorrence of bugs, especially those in my own code. Software contracts are purposed to eliminating ambiguity at software interfaces by being explicit about assumptions and expectations, and enforcing these directly in the code itself. Through these means an entire class of bugs can be pre-engineered out of programs—or at least exposed and dispatched quickly.

The mechanism and techniques presented here do not by any means represent full support for Design by Contract in PL/SQL. In fact, Bertrand Meyer created an entire programming language called Eiffel and a development environment to support it, precisely because existing languages and tools were not entirely capable of fully supporting this paradigm. However, we must live with PL/SQL and do the best we can, and I have tried to present a practical approach to adopting the Design by Contract philosophy within a PL/SQL context.

In wrapping up, I will briefly discuss a few contract-oriented principles for developing bug-free code.

Assert Preconditions Rigorously

PL/SQL programs typically execute their algorithms on simple or complex data structures passed to them via input parameter values or available to them as package global variables. They are no different than programs written in any other language in this respect. Program algorithms typically have very specific requirements about the data on which they execute and may very well fail or give unreliable results when those requirements are not met. These requirements are precisely what Design by Contract refers to as preconditions and enforcing precondition requirements rigorously is the first and best step that you can take to insuring that your algorithms will execute successfully every time. This probably sounds obvious, yet how many programs are written with detailed attention to identifying each and every assumption about the inputs that its algorithms make and documents them explicitly? And of these, how many actually enforce those requirements in the code itself, such that invalid inputs cannot get past the front door, so to speak?

As mentioned several times already, many algorithms are not designed to operate on NULL inputs, or they employ special case code producing special case outputs where they are. Why not simply excise NULL values altogether and limit such programs to the straightforward cases they are designed for? This is not to say that NULL inputs can always be outlawed, but perhaps that is where contract negotiations should begin. If clients absolutely cannot comply then perhaps the precondition will need to be relaxed and the program adapted appropriately, but at least then you will know why it is the case.

It is also good practice to tease out and explicitly enforce each separate precondition rather than overload multiple preconditions into one assertion. For instance, if two input parameters x and y are required to have non-NULL values and x must be greater than y, all three preconditions can be enforced with

assertpre(x > y,'x > y', l_module)

This assertion will fail if either x or y (or both) is NULL; however, these requirements are not explicit in the assertion and must be inferred. Contract-orientation is about being explicit and precise, with no tolerance for inference and innuendo. So there should be three precondition assertions in this example.

The previous example also illustrates a major benefit of assertions I have not emphasized so much, which is that they not only enforce but also document software contracts. If programmers writing code to use your modules can read the source, requirements for proper usage can be read directly from the preconditions.

Require good inputs explicitly, and good outputs are much easier to guarantee.

Modularize Ruthlessly

Having required of callers that they provide good inputs and enforced these requirements in your code, your modules are obliged to produce correct outputs. Programs with hundreds or thousands of lines of code making many transformations to data structures are difficult or impossible to verify for correctness. However, compact computational units with very specific algorithms over well understood and verified data structures may actually have the property of being “self-evidently correct.” This can only be achieved through modularization, which is to say breaking large and complex computations down into smaller and smaller self-contained computational sub-components, and then stringing these together to achieve the larger goals. Each sub-component should be algorithmically as simple as possible, such that confidence in its correctness can be gained through inspection and a minimum of mental gymnastics.

Note that modularizing code may not always mean breaking it into independent executable modules; it could be organizing a single large program into a set of sub-transformations that progressively develop intended outputs from supplied inputs. In these cases, you will want to make assertions on intermediate computational states as one sub-transformation ends and another begins.

By modularizing your code into more and smaller components, you create more interfaces and more opportunities to express contracts. If you adhere to the first principle of rigorous precondition testing, this means there will be more and more assertions built into your code, protecting the contractual requirements of all these interfaces. Each of these assertions is a correctness test executed at runtime: your code becomes hardened like tempered steel in the fire of assertions. How can bugs possibly survive?

Simple and self-evidently correct algorithms operating on trusted data is the virtuous path to bug-free code.

Adopt Function-Based Interfaces

Having adopted the first two principles, what should your simple and self-evidently correct modules look like? Functions offer distinct advantages to enforcing contracts whenever modules must pass computational results back to callers. These advantages were alluded to earlier but are worth reiterating.

  • Functions provide a single output and are thus consistent with singularity of purpose.
  • Functions can be written such that postcondition assertions can be located precisely, immediately preceding the RETURN statement.
  • Functions can be used interchangeably with variables or conditional tests such that highly compact and expressive code is possible, especially when they assert NON NULL postconditions.

This is not to say procedures do not have their place and that you should avoid them. However, much of the PL/SQL code I have seen is procedure-heavy and function-light. When requirements are such that specific input data is computationally transformed and some output data returned, function-based interfaces offer better opportunities to adopt a rigorous contract orientation.

Crash on ASSERTFAIL

In Design by Contract, assertion failures signal bugs, period. Programs that catch ASSERTFAIL exceptions and handle them in some way that masks or hides their occurrence are effectively lying about what is going on: a correctness bug has been identified yet it is not being reported. Better to allow the program to crash and use the diagnostic information provided in the exception message to find and fix the bug.

In the words of Andy Hunt and Dave Thomas (see References above):

Dead Programs Tell No Lies

If you do find yourself compelled to catch and handle ASSERTFAIL because correct computation can proceed in spite of it, then perhaps there are contract issues to iron out. Contract-orientation does not mean exercising dictatorial control over client modules through non-negotiable preconditions. Contracts are an agreement between two parties, and the terms need to be acceptable on both sides. So, rather than catching and handling ASSERTFAIL, it is much better to renegotiate contracts wherever possible.

Regression Test Your Postconditions

Much emphasis has been placed on rigorous assertion of preconditions and far less on postconditions. The requirement to absolutely guarantee output correctness given the preconditions can be difficult at best in most cases, and in some almost impossible. The example provided by the function even is unusual, where having an alternative algorithm for the complementary function odd provides a means of asserting output correctness. And even when possible, the computational cost of asserting complete postcondition correctness is likely to effectively double the cost of computing the output itself.

However, there is a common case in which correct outputs for given inputs is well known, and that is unit testing. Typical unit testing consists of matching program outputs to well-known expected results under specific sets of inputs. This effectively amounts to asserting that the program has met postconditions given these inputs. So hopefully if you are doing production-level PL/SQL programming, you have a testing framework in place and are validating correctness of your modules through regular regression testing.

One caveat here is that only externally declared modules can be regression tested. A possible workaround is to have two versions of the package specification: one that exposes only the modules intended for external use and another that exposes internal modules as well for regression tests. Both versions should, of course, use the same package body, and great care must be taken not to diverge the specifications except in the modules declared. It’s not ideal, but it is possible.

Avoid Correctness-Performance Tradeoffs

Finally, a cautionary principle: avoid what I call the false correctness-performance tradeoff. If you adopt the contract-oriented approaches recommended here, you may find that not only does rigorous assertion of contract elements cause you lots of extra typing, each call to assert is actual code executed and, while extremely inexpensive, is not entirely free. Modules that assert long lists of complex preconditions many thousands of times (perhaps over many rows of a cursor loop) could see these assertions show up as “expensive” when code is profiled.

In such cases, you may be tempted to remove these assertions on the basis of their computational cost; my recommendation is to avoid this temptation as much as possible. Assertions are about insuring correctness of code; performance is about executing code quickly. There is no such thing as a correctness-performance tradeoff: incorrect results obtained quickly are still incorrect and therefore useless.

However, if the performance impact of contract assertions seems too high to tolerate and the modules involved are highly trusted, it may become necessary to relax this dictum and stop testing some assertions. Rather than removing them altogether from the source code, it’s much better to simply comment them out. Remember that assertions in source code are also important documentation artifacts of the contracts to which a module adheres.

Alternatively, in Oracle 10g and higher releases, you can somewhat have your cake and eat it too using the PL/SQL conditional compilation feature. Consider the following procedure that conditionally includes a precondition assertion whenever compiled with the conditional compilation flag DBC is set to TRUE:

PROCEDURE proc02
  IS
    l_module VARCHAR2(30) := 'PROC02';
  BEGIN
    -- conditional precondition assertion
    $IF $$DBC $THEN
    assertpre(FALSE,'Assert FALSE precondition',l_module);
    $END
    
    -- procedure logic
    null;
  END proc02;

The procedure can be compiled with the flag set as TRUE for testing purposes and recompiled with it set as FALSE for releasing to production. In production, if program issues are suspected, the flag can be set back to TRUE and code recompiled to re-enable the assertion testing for bug discovery and diagnosis. Again, this is not recommended as correctness testing in production is where it matters most, but it is possible in situations where the impact on performance is unacceptably high.

Oracle 11g Optimized Compilation

Oracle 11g introduced optimized compilation of PL/SQL including subprogram inlining that can gain significant performance advantages where small local programs are frequently called. An in-depth discussion of the topic is beyond the scope of this chapter, but basically inlining can replace calls to modules local to the compilation unit with a copy of the called module’s code “inline” with the calling code and thereby eliminating call stack overhead.

Inlining is preferentially performed for smaller and simpler local modules, which exactly describes the package-local assertion mechanism I have been using and recommending for years now. Finally, some real vindication for my unorthodox preference to localize the assertion inside each package! So adopting the package-local assertion and enabling full optimization can minimize the cost of enforcing software contracts in your PL/SQL code. Sounds like a win to me.

PL/SQL inlining is enabled by setting the PLSQL_OPTIMIZE_LEVEL parameter to value 3 prior to compilation, as below:

ALTER SESSION SET PLSQL_OPTIMIZE_LEVEL=3;

You can be informed about inlining optimizations and other compiler warnings using the PLSQL_WARNINGS parameter, like so:

ALTER SESSION SET PLSQL_WARNINGS='ENABLE:ALL';

It will be interesting to experiment and measure the performance benefits of PL/SQL inlining on packages heavily laden with contract enforcing assertions.

Summary

Design by Contract is a powerful software engineering paradigm for engineering bugs related to module interfaces (APIs) out of your code. Software contracts are specified by preconditions representing requirements imposed on calling modules, and postconditions representing obligations imposed on the called module. These contract elements are enforced by a software mechanism called assert. Assertion failures always signal bugs in the code in either the calling or called module, depending on whether a precondition or postcondition has been violated, respectively.

Adopting a contract-oriented approach to PL/SQL can be achieved using a simple, standardized assertion mechanism and adopting disciplined, standardized usage across the body of your code. This chapter showed you specific techniques I developed for doing just that, and I sincerely hope you find these techniques valuable should you choose to adopt them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset