Design by Contract
- Expect a certain condition to be guaranteed on entry by any client module that calls it: the routine’s precondition —an obligation for the client, and a benefit for the supplier (the routine itself), as it frees it from having to handle cases outside of the precondition.
- Guarantee a certain property on exit: the routine’s postcondition — an obligation for the supplier, and obviously a benefit (the main benefit of calling the routine) for the client.
- Maintain a certain property, assumed on entry and guaranteed on exit: the class invariant.
An Apex Example
Let’s look at a small example of how a contract can be added to an Apex method. Here’s a simple method that returns the set of unique values for a field in an array of SObjects. Notice that we’re enforcing 3 aspects of our ‘contract’:
- the ‘field’ parameter may not be null
- the returned list will not be null
- the returned list will not include a ‘null’ value
global class SF {
/**
* @description return the unique values for a given field in a list of
* records. Null is not included.
* @param objects the list of records
* @param field values from this field will be returned
* @return set of values
*/
public static Set getFieldValues(SObject[] objects, SObjectField field) {
SF.preCondition(field != null, 'SF.getFieldValues() - field is required');
Set result = new Set();
if (!SF.isEmpty(objects)) {
final String fieldName = field.getDescribe().getName();
for (SObject o : objects) {
result.add(String.valueOf(o.get(fieldName)));
}
result.remove(null);
}
SF.postCondition(result != null, 'SF.getFieldValues() - null result');
SF.postCondition(!result.contains(null), 'SF.getFieldValues() - null value');
return result;
}
}
We can also add contract checking to our getFieldValues unit test.
@isTest
public class SF_Test {
static testMethod void testGetFieldValues() {
User[] users = new User[] {
SF_Test.newUser('0@x.com'),
SF_Test.newUser('1@x.com'),
SF_Test.newUser('2@x.com'),
SF_Test.newUser('3@x.com'),
SF_Test.newUser('4@x.com'),
SF_Test.newUser('5@x.com')
};
insert users;
Test.startTest();
// test preConditions
try {
SF.getFieldValues(users, null);
System.assert(false, 'field is required');
} catch (SF.AssertionException e) {
}
// test postConditions
Set names = SF.getFieldValues(null, User.UserName);
System.assert(null != names);
names = SF.getFieldValues(users, User.MobilePhone);
System.assert(!names.contains(null));
// successful call with unique values
names = SF.getFieldValues(users, User.UserName);
for (User u : users) {
System.assert(names.contains(u.UserName));
}
Test.stopTest();
}
}
Apex Contract Implementation
Many languages have facilities to make contract assertions. Unfortunately, Apex isn’t one of them, so we’ll create our own. We’ll create preCondition, postCondition, and invariant methods to describe our contracts. Each of these will throw an exception if the contract is broken, which will provide immediate and obvious feedback to the developer. Throwing exceptions instead of using System.assert also makes it possible for our unit tests to test invalid conditions.
Contract conditions should never be violated during execution of production code. Contracts are therefore typically only checked in debug mode during software development. After being deployed to production, the contract checks are disabled to maximize performance. In order to allow this, we’ve added a ‘debugging__c’ custom setting.
global class SF {
// @description base class for all exceptions
public abstract class SF_Exception extends Exception {
}
// @description a failed assertion
public class AssertionException extends SF_Exception {
}
//--------------------------------------------------------------------------
// Diagnostics
public static Boolean debugging {
get {
return SF__c.getInstance().debugging__c;
}
}
public static void preCondition(Boolean condition, Object message) {
assert(condition, 'preCondition failed: ' + message);
}
public static void postCondition(Boolean condition, Object message) {
assert(condition, 'postCondition failed: ' + message);
}
public static void invariant(Boolean condition, Object message) {
assert(condition, 'invariant failed: ' + message);
}
public static void assert(Boolean condition, Object message) {
if (debugging && !condition) {
throw new SF.AssertionException(String.valueOf(message));
}
}
}
The following unit test ensures that the contract methods are working and can be disabled by the custom setting.
public static Boolean debugging {
get {
return (null != SF__c.getInstance()) ?
SF__c.getInstance().debugging__c : false;
}
set {
SF__c prefs = SF__c.getInstance();
if (null == prefs) {
prefs = new SF__c();
}
prefs.debugging__c = value;
upsert prefs;
}
}
static testMethod void testDiagnostics() {
Test.startTest();
// with debugging disabled, none of these fire
debugging = false;
SF.preCondition(false, 'message');
SF.postCondition(false, 'message');
SF.invariant(false, 'message');
// when debugging is enabled, they do fire
debugging = true;
try { SF.preCondition(false, 'message'); System.assert(false); }
catch (SF.AssertionException e) {}
try { SF.postCondition(false, 'message'); System.assert(false); }
catch (SF.AssertionException e) {}
try { SF.invariant(false, 'message'); System.assert(false); }
catch (SF.AssertionException e) {}
Test.stopTest();
}