Contract-Oriented PL/SQL Coding

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
,message in varchar2 := null
)
is
begin
if not nvl (condition, false)
then
raise_application_error (assertfail_c
,'ASSERTFAIL: '||package_name
||': '||substr (nvl (message, 'No Message'), 1, 200)
);
end if;
end assert;

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

   assertfail_c constant integer := -20999;
assertfail exception ;
pragma exception_init (assertfail, -20999);

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

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

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
,p_deptno in number
)
is
begin
$if $$assert
$then
assert (p_ename is not null
,'The Ename must be Filled'
);
assert ((p_deptno is not null and p_deptno > 0)
,'The Deptno must be greater than zero'
);
$end
...
end proc;

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.
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 OO (object-oriented) languages don’t support directly the ideas behind DBC.
However, precondition and postcondition are applicable to many programming languages, both OO 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 <= count
count_bounded: count <= capacity
empty_if_no_elements: empty = (count = 0)

end -– class interface STACK



PL/SQL "equivalent":

-- Stack implementation - TABLE of INTEGER.
-- Eiffel has generic classes (like C++ templates and better than Java generics).
-- PL/SQL has not generic classes (or generic packages;
-- PL/SQL was modeled after ADA 83, and ADA 83 has generic packages).
--
CREATE OR REPLACE TYPE array_t AS TABLE OF INTEGER
/


-- utility package:
CREATE OR REPLACE PACKAGE dbc AS
-- 0 = no check
-- 1 = check preconditions
-- 2 = check preconditions + postconditions
-- 3 = check preconditions + postconditions + invariants
no_check CONSTANT INTEGER := 0;
check_preconditions CONSTANT INTEGER := 1;
check_pre_postconditions CONSTANT INTEGER := 2;
check_pre_post_invariants CONSTANT INTEGER := 3;

FUNCTION checking_level RETURN INTEGER;
PROCEDURE set_level (p_level INTEGER);
PROCEDURE display_error (p_error VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY dbc AS
m_level INTEGER := 0;

FUNCTION checking_level RETURN INTEGER IS
BEGIN
RETURN m_level;
END;

PROCEDURE set_level (p_level INTEGER) IS
BEGIN
IF p_level NOT IN
(no_check, check_preconditions, check_pre_postconditions, check_pre_post_invariants)
THEN
RAISE_APPLICATION_ERROR (-20000, 'Wrong checking level');
END IF;

m_level := p_level;
END;

PROCEDURE display_error (p_error VARCHAR2) IS
BEGIN
RAISE_APPLICATION_ERROR (-20000, 'ERROR in method ' || p_error);
END;
END;
/


CREATE OR REPLACE TYPE stack AS OBJECT (
-- Maximum number of stack elements
capacity INTEGER,

-- Number of stack elements
el_count INTEGER,

-- Stack implementation
stack_implementation array_t,

-- Alocate stack for a maximum of n elements
CONSTRUCTOR FUNCTION stack (n INTEGER) RETURN SELF AS RESULT,
-- 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 >= dbc.check_preconditions AND n < 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 >= dbc.check_pre_postconditions 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 >= 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 >= 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 >= dbc.check_pre_postconditions 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 >= 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 >= 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 >= 0 THEN RETURN TRUE; ELSE RETURN FALSE; END IF; END; MEMBER FUNCTION count_bounded RETURN BOOLEAN IS BEGIN IF el_count <= 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 <> 0) THEN RETURN TRUE; ELSE RETURN FALSE; END IF; END; MEMBER PROCEDURE check_invariants IS BEGIN IF dbc.checking_level < 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

5 Comments

  1. Zlatko Sirotic March 20, 2007
  2. Alex Nuijten March 19, 2007
  3. Zlatko Sirotic March 18, 2007
  4. Zlatko Sirotic March 18, 2007
  5. Karl March 16, 2007