Programming By Contract

What Is Programming By Contract?

Programming by contract is a correctness approach built on a 3-step scientific process:

  1. require that all pre-conditions are met
  2. do the work
  3. ensure that all post-conditions are met

Pre-conditions and post-conditions can include:

Checks can also be made whilst the module / function / method is doing the work.

Why Is Programming By Contract A Good Idea?

Programming by contract builds proven correctness into the code that you ship:

This approach catches errors in your code as early as possible.

The greater the distance between when an error occurred and where an error is detected, the harder it is to correctly identify the real cause of the error.

Programming by contract moves responsibility for correctness from your unit tests and into the code that you ship. This makes it a lot easier for other developers to know that they're using your code safely.

With programming by contract, unit tests become responsible for exercising the code and documenting all of the features and functionality that your code provides.

With programming by contract, your correctness tests are also there when your code goes through functional and non-functional testing. That's something you can't get by placing the burden of proof on your unit tests alone.

Why Is Programming By Contract Different From Input Validation?

Most widely-used robustness approaches focus primarily on validating inputs. This is often driven by security concerns (preventing data breaches, preventing the host being compromised, preventing denial-of-service). These are all important activities, but they are examples of non-functional requirements.

They're not driven by ensuring that the delivered code actually works.

Separating Robustness From Correctness

Separate out the expectations and guarantees into two distinct layers:

Use PHP's type-checking support to perform robustness checks. PHP 7 is a major step forward in this area. Use inspection / reflection libraries like our own Reflection Library to plug gaps in what the language can check. Implement robustness checks on every public method.

What Contracts To Define And Where

Limit your correctness checks to the context where the contract is being enforced. For example, an API library should not attempt to enforce any contract about business logic.

Code Type Robustness? Correctness?
Library Yes Library state only
Framework Yes Framework state only
Controllers Yes Application route only
Business Logic Yes Business rules only
Data Layer Yes Data integrity only
Views Yes No

If you add contracts that check things they have no business knowing about, you'll find that the contracts get in the way of reusing your code. Getting the balance right takes time and experience.

Meeting Expectations

Use the Contracts class to check pre-conditions and post-conditions:

use GanbaroDigital\Contracts\V1\Contracts;

function cancelDirectDebit(DirectDebit $mandate)
{
    // correctness!
    Contracts::requireThat(function() use ($mandate) {
        Contracts::assertValue($mandate, !$mandate->isCancelled(), "mandate is already cancelled");
    });

    // cancel the mandate here
    //
    // it isn't the API client's role to check whether or not a mandate should
    // be cancelled ... it's just going to pass through the details to the
    // remote API
    $this->apiClient->cancelMandate($mandate);

    // correctness!
    Contracts::ensureThat(function() use ($mandate) {
        Contracts::assertValue($mandate, $mandate->isCancelled(), "unable to cancel mandate");
    });
}

Fail Early, Fail Hard

When an expectation is not met, an exception is thrown straight away. Errors are caught as soon as possible, and invalid values are not allowed to propagate through your application. The idea here is to make sure that bugs in your code are identified as quickly as possible.

The greater the distance between when an error occurred and where an error is detected, the harder it is to correctly identify the real cause of the error.

If you're the kind of programmer who does this:

try {
    // do something ...
}
catch (\Exception $e) {
    // log exception
    // do nothing
}

then you'll get limited benefits from programming by contract. The same is true if your app consumes a lot of libraries which catch all exceptions - especially if they're silently swallowing errors.