Wednesday, August 21, 2013

Apex Contract Programming

Design by Contract

"Design by Contract" was coined by Bertrand Meyer to describe a feature of his Eiffel programming language. It has its roots in formal verification and specification. The primary idea is that elements of a software system collaborate with each other on the basis of mutual obligations and benefits. They agree on a "contract" that, for example:
  • 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.
In lay terms, adding contracts to your code ensures that methods are called and behave correctly.

Design by contract does not replace regular testing strategies, such as unit testing, integration testing, and system testing. Rather, it complements external testing with internal self-tests that can be activated both for isolated tests and in production code during a test-phase. The advantage of internal self-tests is that they can detect errors before they manifest themselves as invalid results observed by the client. This leads to earlier and more specific error detection.

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':
  1. the 'field' parameter may not be null
  2. the returned list will not be null
  3. 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<string> getFieldValues(SObject[] objects, SObjectField field) {
    SF.preCondition(field != null, 'SF.getFieldValues() - field is required');
   
    Set<string> result = new Set<string>();
    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<string> 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();
  }
}

Now any developer that calls getFieldValues incorrectly will be notified immediately, with an informative message. Our post conditions also ensure that future modifications to the method will not break our contract with the caller.

Next, we'll look at the implementation of our contract methods.

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();
  }

Usage Warning

Contracts should never be used to handle user or run time errors. They are meant to ensure method calling and behavioral correctness, much as a compiler enforces syntax correctness. Be sure to provide appropriate error handling where necessary.

No comments:

Post a Comment