Contract-Oriented PL/SQL Coding

5

During the Oracle PL/SQL Programming Conference in San Francisco another session I really liked was called "Contract-oriented PL/SQL Coding".
In this blog I will go over the basic principles of Contract-Oriented PL/SQL coding as suggested by John Beresniewicz, who did this presentation.

....
Contract-Oriented Coding originates from the Eiffel programming language. This OO-programming language was designed based on the DbC (Design by
Contract) principles. Some of the design principles can easily be adopted by PL/SQL.
The principles are quite simple; before you write the program make a "contract" of assumptions and obligations.
When you write code, as least when I write code, assumptions are made whether deliberate or not.
You make assumptions like it is "always a positive number" or "this parameter can never be null" or even "this parameter
will always contain either the value ‘x’ or ‘z’". Instead of just making these assumptions, make them explicit.

Pre- and Postconditions

Software modules have a Client-Supplier relationship. Because of this relation, you could say there exists a contract between the two. Both
sides of the contract have benefits and obligations. Adhering to this contract promotes reliability and maintainability.

The first thing you do is check the parameters coming into the program. When the parameter values don’t meet the requirements, raise
an exception. The calling program should only provide those values that the called program expects. Testing the incoming data is done through assertions.
The other side of the contract, the obligations, relate to the offered functionality. Before you start coding, the requirements
of the program should be clear. How can you otherwise create a program, right? When you call a program, you should treat it as a
black box. There are certain things that you expect from a program and the program should adhere to them.
Postcondition Assertions are the conditions that should be true when a program module is completed. The called program should conform to the functionality that is expected from it.

Prior to passing control back to the calling program, postcondition assertions should be done to verify the correctness of your program. The PL/SQL language is not really equipped to perform postcondition assertions. John recommends not to perform these assertions. This doesn’t mean you shouldn’t test
your code, it simply means don’t test your code this way. It wouldn’t make much sense to reimplement the functionality that you created just to perform a postcondition assertion.

"Do Nothing or Complain Loudly"

To make sure that the incoming parameters adhere to the contract, the parameter values should be validated through assertions.
Assertions are simple procedures that accept a boolean condition and an optional message.
The functionality that the assertion should provide is simply put:

Do Nothing: when the incoming condition adheres to the contract
Complain Loudly: raise a predefined exception when the incoming condition does not adhere to the contract.

An assertion procedure would look like this:

procedure assert (condition in boolean<br />                 ,message   in varchar2 := null<br />                 )<br />is<br />begin<br />   if not nvl (condition, false)<br />   then<br />      raise_application_error (assertfail_c<br />                              ,'ASSERTFAIL: '||package_name<br />                               ||': '||substr (nvl (message, 'No Message'), 1, 200)<br />                              );<br />   end if;<br />end assert;<br />

The assertfail_c would be a global constant with the exception number, e.g.

   assertfail_c constant integer := -20999;<br />   assertfail exception ;<br />   pragma exception_init (assertfail, -20999);<br />

Just to show you an example of how to use an assertion procedure:

procedure proc (p_ename in varchar2<br />               ,p_deptno in number<br />               )<br />is<br />begin<br />   assert (p_ename is not null<br />          ,'The Ename must be Filled'<br />          );<br />   assert ((p_deptno is not null and p_deptno &gt; 0)<br />          ,'The Deptno must be greater than zero'<br />          );<br />   ...<br />end proc;<br />

When you try to use this procedure, and you would pass in a NULL for the p_ename parameter, an exception would be raised (complain loudly). It also provides
you with feedback about the exception and how to resolve it.

"Dead Programs tell no lies"

The predefined Assertion Exception should not be handled in any of the programs, otherwise it would be impossible to "complain loudly". This is equally true
for the WHEN OTHERS exception handlers. If an exception is smothered by a WHEN OTHERS, no one will ever know about the exception. Just let it propagate out!
In Oracle 11g this will generate a compile time warning.

Some Guidelines

John Beresniewicz who presented this session, made some general recommendation on implementing this DbC principle in PL/SQL.
Instead of implementing both the pre- and post-condition assertions, only implement the preconditions. Creating a post-condition assertion may
take the same amount of time and effort, or even more, than creating the original program to begin with.
On aggressively asserting preconditions: "like a double knot on your sneakers".

"Modularize ruthlessly and coherently"
When you create lots of small modules which asserts preconditions, you  can create a hierarchy of "assertion-hardened code".  A top level module can be trusted because the lower
levels all conform to the predefined contracts.

Assertion Hardened Code Hierarchy 

Conclusion

To paraphrase John Beresniewicz: "The first time you see an exception that originates from a breach of contract, that’s when
you realize the power of using the Contract-Oriented Coding Technique".
Does the assertion code add overhead? Of course there is overhead by using assertions in your code, I don’t have exact numbers. John had a great analogy regarding leaving the assertion code in the code when going to production:
"It is like wearing a life-vest only when you are in training and not while you’re at open sea."
If you really want to get rid of the assertion code, you could opt to use conditional compilation (available since Oracle 10gR2).
Your code would look like the section below:

procedure proc (p_ename in varchar2<br />               ,p_deptno in number<br />               )<br />is<br />begin<br />   $if $$assert<br />   $then<br />      assert (p_ename is not null<br />             ,'The Ename must be Filled'<br />             );<br />      assert ((p_deptno is not null and p_deptno &gt; 0)<br />             ,'The Deptno must be greater than zero'<br />             );<br />   $end<br />   ...<br />end proc;<br />

Special Thanks

Special thanks to John Beresniewicz who was kind enough to allow me to include his demonstration package with this post. This package is all about Prime numbers. It shows how the DbC can be implemented and the feedback you will get when you supply out of range values for the input parameters.

References

Correction 14 March 2007 16:42

I just received an email from John to make a small correction. Instead of editing the original text I decided to include his email here:

Alex –
Nice blog. One thing I want to note is that the "life-vest" analogy is
not mine but rather I just repeated (actually re-read from the Bertrand
Meyer book) a quote attributed to the famous computer scientist C.A.R.
Hoare. So maybe correct that if you can as I do not want to appear to
take credit for anything not of my origination.

One of the takeaways I hope people got from the presentation is that
there are very powerful ideas out there in the larger world of other
programming languages and theoretical computer science which broaden
our perspective and which can sometimes be adapted (even if only
partially) to PL/SQL programming. Just because concepts and techniques
are not directly supported by PL/SQL language constructs does not mean
they are irrelevant to PL/SQL programming.
JB

Comment by Zlatko Sirotic (but formatted properly)

Design by contract (DBC) is a method whose author is Bertrand Mayer, also maker of OOPL language Eiffel.<br />Simplified, DBC is based on principle that in each routine (procedure or function) with standard code,<br />two additional parts – PRECONDITION and POSTCONDITION - need to be asserted.<br />An additional assertion in class is called INVARIANT.<br />Contract is based on routine's putting up an obligation to caller (to some other routine)<br />to satisfy conditions of precondition and conditions of invariant, and hers (called routine's) obligation<br />to satisfy conditions of postcondition and conditions of invariant.<br /><br />The object oriented Eiffel programming language was created to implement DBC.<br />For now, other OO (object-oriented) languages don’t support directly the ideas behind DBC.<br />However, precondition and postcondition are applicable to many programming languages, both OO and not OO.<br />Invariants are applicable only in OOPL.<br /><br />This is my attempt to use DBC methodology (including invariants) in Oracle PL/SQL :)<br /><br /><br /><br />Class interface from Bertrand Meyer's book &quot;Object oriented software construction&quot;, second edition (OOSC2), page 390-391:<br /><br />class interface STACK [G]<br /><br />creation make<br /><br />feature -- Initialization<br />  make (n: INTEGER) is -- Alocate stack for a maximum of n elements<br />    require<br />      non_negative_capacity: n &gt;= 0<br />    ensure<br />      capacity_set: capacity = n<br />    end<br /><br />feature -- Access<br />  capacity: INTEGER -- Maximum number of stack elements<br /><br />  count: INTEGER -- Number of stack elements<br /><br />  item: G is -– Top element<br />    require<br />      not_empty: not empty<br />    end<br /><br />feature -- Status report<br />  empty: BOOLEAN is –- Is stack empty?<br />    ensure<br />      empty_definition: Result = (count = 0)<br />    end<br /><br />  full: BOOLEAN is –- Is stack full?<br />    ensure<br />      full_definition: Result = (count = capacity)<br />    end<br /><br />feature -- Element change<br />  put (x: G) is –- Add x on top<br />    require<br />      not_full: not full<br />    ensure<br />      not_empty: not empty<br />      added_to_top: item = x<br />      one_more_item: count = old count + 1<br />    end<br /><br />  remove is -– Remove top element<br />    require<br />      not_empty: not empty<br />    ensure<br />      not_full: not full<br />      one_fewer: count = old count - 1<br />    end<br /><br />invariant<br />  count_non_negative: 0 &lt;= count<br />  count_bounded: count &lt;= capacity<br />  empty_if_no_elements: empty = (count = 0)<br /><br />end -– class interface STACK<br /> <br /><br /><br />PL/SQL &quot;equivalent&quot;:<br /><br />-- Stack implementation - TABLE of INTEGER.<br />-- Eiffel has generic classes (like C++ templates and better than Java generics).<br />-- PL/SQL has not generic classes (or generic packages;<br />-- PL/SQL was modeled after ADA 83, and ADA 83 has generic packages).<br />--<br />CREATE OR REPLACE TYPE array_t AS TABLE OF INTEGER<br />/<br /><br /><br />-- utility package:<br />CREATE OR REPLACE PACKAGE dbc AS<br />  -- 0 = no check<br />  -- 1 = check preconditions<br />  -- 2 = check preconditions + postconditions<br />  -- 3 = check preconditions + postconditions + invariants<br />  no_check                  CONSTANT INTEGER := 0;<br />  check_preconditions       CONSTANT INTEGER := 1;<br />  check_pre_postconditions  CONSTANT INTEGER := 2;<br />  check_pre_post_invariants CONSTANT INTEGER := 3;<br /><br />  FUNCTION  checking_level RETURN INTEGER;<br />  PROCEDURE set_level (p_level INTEGER);<br />  PROCEDURE display_error (p_error VARCHAR2);<br />END;<br />/<br /><br />CREATE OR REPLACE PACKAGE BODY dbc AS<br />  m_level INTEGER := 0;<br /><br />  FUNCTION checking_level RETURN INTEGER IS<br />  BEGIN<br />    RETURN m_level;<br />  END;<br /><br />  PROCEDURE set_level (p_level INTEGER) IS<br />  BEGIN<br />    IF p_level NOT IN<br />      (no_check, check_preconditions, check_pre_postconditions, check_pre_post_invariants)<br />    THEN<br />      RAISE_APPLICATION_ERROR (-20000, 'Wrong checking level');<br />    END IF;<br /><br />    m_level := p_level;<br />  END;<br /><br />  PROCEDURE display_error (p_error VARCHAR2) IS<br />  BEGIN<br />    RAISE_APPLICATION_ERROR (-20000, 'ERROR in method ' || p_error);<br />  END;<br />END;<br />/<br /><br /><br />CREATE OR REPLACE TYPE stack AS OBJECT (<br />  -- Maximum number of stack elements<br />  capacity INTEGER,<br /><br />  -- Number of stack elements<br />  el_count INTEGER,<br /><br />  -- Stack implementation<br />  stack_implementation array_t,<br /><br />  -- Alocate stack for a maximum of n elements<br />  CONSTRUCTOR FUNCTION stack (n INTEGER) RETURN SELF AS RESULT,<br />
  -- Top element
  MEMBER FUNCTION item (SELF IN OUT stack) RETURN INTEGER,

  -- Is stack empty?
  MEMBER FUNCTION empty RETURN BOOLEAN,

  -- Is stack full?
  MEMBER FUNCTION full RETURN BOOLEAN,

  -- Add x on top
  MEMBER PROCEDURE put (x INTEGER),

  -- Remove top element
  MEMBER PROCEDURE remove,

  -- INVARIANTS
  -- If subprogram is declared in an object type body
  -- it must be defined in the object type specification too.
  MEMBER FUNCTION count_non_negative RETURN BOOLEAN,
  MEMBER FUNCTION count_bounded RETURN BOOLEAN,
  MEMBER FUNCTION empty_if_no_elements RETURN BOOLEAN,
  MEMBER PROCEDURE check_invariants
) NOT FINAL;
/

CREATE OR REPLACE TYPE BODY stack AS
  CONSTRUCTOR FUNCTION stack (n INTEGER) RETURN SELF AS RESULT IS
  BEGIN
    IF dbc.checking_level &gt;= dbc.check_preconditions AND n &lt; 0 THEN
      dbc.display_error ('stack - PRE');
    END IF;

    check_invariants;

    capacity := n;
    stack_implementation := array_t();
    stack_implementation.EXTEND (n);

    IF dbc.checking_level &gt;= dbc.check_pre_postconditions AND capacity &lt;&gt; n THEN
      dbc.display_error ('stack - POST');
    END IF;

    check_invariants;
  END;

  MEMBER FUNCTION item (SELF IN OUT stack) RETURN INTEGER IS
  BEGIN
    IF dbc.checking_level &gt;= dbc.check_preconditions AND empty THEN
      dbc.display_error ('item - PRE');
    END IF;

    check_invariants;

    RETURN stack_implementation(el_count);
  END;

  MEMBER FUNCTION empty RETURN BOOLEAN IS
  BEGIN
    IF el_count = 0 THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER FUNCTION full RETURN BOOLEAN IS
  BEGIN
    IF el_count = capacity THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER PROCEDURE put (x INTEGER) IS
  BEGIN
    IF dbc.checking_level &gt;= dbc.check_preconditions AND full THEN
      dbc.display_error ('put - PRE');
    END IF;

    check_invariants;

    el_count := el_count + 1;
    stack_implementation(el_count) := x;

    -- PL/SQL has not Eiffel's OLD
    -- one_more_item: count = old count + 1
    IF dbc.checking_level &gt;= dbc.check_pre_postconditions AND (empty OR item &lt;&gt; x) THEN
      dbc.display_error ('put - POST');
    END IF;

    check_invariants;
  END;

  MEMBER PROCEDURE remove IS BEGIN
    IF dbc.checking_level &gt;= dbc.check_preconditions AND empty THEN
      dbc.display_error ('remove - PRE');
    END IF;

    check_invariants;

    el_count := el_count - 1;

    -- PL/SQL has not Eiffel's OLD
    -- one_fewer: count = old count - 1
    IF dbc.checking_level &gt;= dbc.check_pre_postconditions AND full THEN
      dbc.display_error ('remove - POST');
    END IF;

    check_invariants;
  END;


  -- INVARIANTS

  MEMBER FUNCTION count_non_negative RETURN BOOLEAN IS
  BEGIN
    IF el_count &gt;= 0 THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER FUNCTION count_bounded RETURN BOOLEAN IS
  BEGIN
    IF el_count &lt;= capacity THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER FUNCTION empty_if_no_elements RETURN BOOLEAN IS
  BEGIN
    IF empty AND (el_count = 0)
       OR
       NOT empty AND (el_count &lt;&gt; 0)
    THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER PROCEDURE check_invariants IS
  BEGIN
    IF dbc.checking_level &lt; dbc.check_pre_post_invariants THEN
      RETURN; -- without checking
    END IF;

    IF NOT count_non_negative THEN
      dbc.display_error ('INVARIANT count_non_negative');
    END IF;

    IF NOT count_bounded THEN
      dbc.display_error ('INVARIANT count_bounded');
    END IF;

    IF NOT empty_if_no_elements THEN
      dbc.display_error ('INVARIANT empty_if_no_elements');
    END IF;
  END;

END; -- class body STACK
/


Best regards,
Zlatko Sirotic
Share.

About Author

5 Comments

  1. Zlatko Sirotic on

    I am sending mail to

    info@amis.nl
    Subject: mail for Alex Nuijten – Response to “Contract-Oriented PL/SQL Coding”

    Best regards,
    Zlatko Sirotic

  2. Alex Nuijten on

    Thank you both for your comments.

    Sorry about that, Zlatko, Apperently the formatting is lost when using comments… if you could mail the source to me, I could place it in the body of the post (of course with credits to you)

  3. Zlatko Sirotic on

    Design by contract (DBC) is a method whose author is Bertrand Mayer, also maker of OOPL language Eiffel.
    Simplified, DBC is based on principle that in each routine (procedure or function) with standard code,
    two additional parts – PRECONDITION and POSTCONDITION – need to be asserted.
    An additional assertion in class is called INVARIANT.

    Contract is based on routine’s putting up an obligation to caller (to some other routine)
    to satisfy conditions of precondition and conditions of invariant, and hers (called routine’s) obligation
    to satisfy conditions of postcondition and conditions of invariant.
    The object oriented Eiffel programming language was created to implement DBC.

    For now, other object-oriented languages don’t support directly the ideas behind DBC.
    However, precondition and postcondition are applicable to many programming languages, both object oriented and not OO.
    Invariants are applicable only in OOPL.

    This is my attempt to use DBC methodology (including invariants) in Oracle PL/SQL :)

    Class interface from Bertrand Meyer’s book “Object oriented software construction”, second edition (OOSC2), page 390-391:

    class interface STACK [G]

    creation make

    feature — Initialization
    make (n: INTEGER) is — Alocate stack for a maximum of n elements
    require
    non_negative_capacity: n >= 0
    ensure
    capacity_set: capacity = n
    end

    feature — Access
    capacity: INTEGER — Maximum number of stack elements

    count: INTEGER — Number of stack elements

    item: G is -– Top element
    require
    not_empty: not empty
    end

    feature — Status report
    empty: BOOLEAN is –- Is stack empty?
    ensure
    empty_definition: Result = (count = 0)
    end

    full: BOOLEAN is –- Is stack full?
    ensure
    full_definition: Result = (count = capacity)
    end

    feature — Element change
    put (x: G) is –- Add x on top
    require
    not_full: not full
    ensure
    not_empty: not empty
    added_to_top: item = x
    one_more_item: count = old count + 1
    end

    remove is -– Remove top element
    require
    not_empty: not empty
    ensure
    not_full: not full
    one_fewer: count = old count – 1
    end

    invariant
    count_non_negative: 0 = 1 AND n = 2 AND capacity n THEN
    dbc.display_error (‘stack – POST’);
    END IF;

    check_invariants;
    END;

    MEMBER FUNCTION item (SELF IN OUT stack) RETURN INTEGER IS
    BEGIN
    IF dbc.checking_level >= 1 AND empty THEN
    dbc.display_error (‘item – PRE’);
    END IF;

    check_invariants;

    RETURN implement(el_count);
    END;

    MEMBER FUNCTION empty RETURN BOOLEAN IS
    BEGIN
    IF el_count = 0 THEN
    RETURN TRUE;
    ELSE
    RETURN FALSE;
    END IF;
    END;

    MEMBER FUNCTION full RETURN BOOLEAN IS
    BEGIN
    IF el_count = capacity THEN
    RETURN TRUE;
    ELSE
    RETURN FALSE;
    END IF;
    END;

    MEMBER PROCEDURE put (x INTEGER) IS
    BEGIN
    IF dbc.checking_level >= 1 AND full THEN
    dbc.display_error (‘put – PRE’);
    END IF;

    check_invariants;

    el_count := el_count + 1;
    implement(el_count) := x;

    — PL/SQL has not Eiffel’s OLD
    — one_more_item: count = old count + 1
    IF dbc.checking_level >= 2 AND (empty OR item x) THEN
    dbc.display_error (‘put – POST’);
    END IF;

    check_invariants;
    END;

    MEMBER PROCEDURE remove IS BEGIN
    IF dbc.checking_level >= 1 AND empty THEN
    dbc.display_error (‘remove – PRE’);
    END IF;

    check_invariants;

    el_count := el_count – 1;

    — PL/SQL has not Eiffel’s OLD
    — one_fewer: count = old count – 1
    IF dbc.checking_level >= 2 AND full THEN
    dbc.display_error (‘remove – POST’);
    END IF;

    check_invariants;
    END;

    — INVARIANTS

    MEMBER FUNCTION count_non_negative RETURN BOOLEAN IS
    BEGIN
    IF el_count >= 0 THEN
    RETURN TRUE;
    ELSE
    RETURN FALSE;
    END IF;
    END;

    MEMBER FUNCTION count_bounded RETURN BOOLEAN IS
    BEGIN
    IF el_count 0)
    THEN
    RETURN TRUE;
    ELSE
    RETURN FALSE;
    END IF;
    END;

    MEMBER PROCEDURE check_invariants IS
    BEGIN
    IF dbc.checking_level

  4. Would it be nice to have this as real language feature like in Eiffel.
    Another point arises here similar to this : programming with aspcects (intrude your own procedures /function with common sense code);
    Wouldn’t it be very nices features for PL/SQL
    Karl