Contracts
Since version 1.4, Cerny.js supports programming by contract (design by contract) in JavaScript. This methodology helps us to improve the documentation of an object or function, by making the contract of usage between the author and consumer explicit. This guide puts us in a position from where we can start programming by contract in JavaScript with the help of Cerny.js.
Preconditions
A function may have a precondition, which is a function checking
that the conditions of a successful call are met. A precondition
is specified with CERNY.pre. The check must be
performed with the function CERNY.check. The first argument
to the check must be an expression evaluating to
true or false. The second argument is a
message stating the nature of the violation in natural
language. This message is intended for human consumption only. If
the expression evaluates to false, the check is said
to have failed and a CERNY.ContractViolation is
thrown.
The arguments of the actual function call are passed to the
precondition. It is also possible to use the keyword
this. It refers to the object on which the function
is called.
The following code illustrates the specification of a precondition. It defines, for the sake of example, a function for dividing two numbers.
var NewMath = {};
(function() {
// Shorten some names
var check = CERNY.check;
var pre = CERNY.pre;
var method = CERNY.method;
// The new division
function divide(a,b) {
return a / b;
}
// For contracts to be enforced, it is necessary
// that a function is subject to interception. Therefore
// CERNY.method must be used.
method(NewMath, "divide", divide);
// The precondition for a division
pre(divide, function(a,b) {
check(b !== 0, "b may not be 0");
});
})();
Postconditions
A postcondition is specified with CERNY.post. The postcondition
itself receives a clone of the object, the return value, and the
arguments of the call in that order. The clone holds the state of
the object before method invocation and is usually referred to as
old.
(function() {
// Defined a set with a remove method, which returns true on a
// remove ...
function remove(element) {
...
}
// The postcondition for a remove
post(remove, function(old, result, element) {
check(result === true && old.length === this.length + 1,
"result may not be true when no element was removed");
check(result === false && old.length === this.length,
"result may not be false, when an element was removed");
check(!this.contains(element),
"element is still in the set");
});
})();
Invariants
An object may have a method called invariant, which
specifies conditions that must be true for that object. The
invariant is called after each method call and enforces a
certain state in the object.
Let's have a look at the following example, which introduces a calendar event, and specifies that the end date may not be before the start date. Again this example only serves the purpose of demonstrating the invariant concept, and may not make a lot of sense in other aspects.
(function() {
var check = CERNY.check;
var method = CERNY.method;
var signature = CERNY.signature;
function Event(start, end) {
this.start = start;
this.end = end;
}
function setStart(start) {
this.start = start;
}
signature(setStart, "undefined", Date);
method(Event.prototype, "setStart", setStart);
function setEnd(end) {
this.end = end;
}
signature(setEnd, "undefined", Date);
method(Event.prototype, "setEnd", setEnd);
// This invariant guarantees a certain state in the object,
// which then can be considered true.
Event.prototype.invariant = function() {
check(this.start.getTime() <= this.end.getTime(),
"start must be before end");
}
})();
Enforcing contracts
For contracts to be enforced the ContractChecker
interceptor must be activated. Refer to the interception guide for further
information. Contract violations are logged at FATAL
level, so set your ROOT
category accordingly. Furthermore they should appear as
exceptions on your browser console.
For a method to take part in contract checking, it must be subject
to interception. Therefore it must be attached to the object with
CERNY.method, or
CERNY.intercept must
be called on the object.
Contracts should only be enforced during development
time. A finalized product, that works correctly, should not
have the ContractChecker enabled, because it is time
consuming and does not contribute to the task. On the other hand,
a product developed with programming by contract will execute
faster, because many checks that are only needed at a development
level are omitted.
Limitations
Currently there is no support for inheritance when checking
invariants. The support for old is weak, because the
algorithm of CERNY.clone
is very naive and needs to be improved. There is no facility which
removes contracts from the productive source code.
