COMP2511

🎨  2.5 - Design by Contract

Thinking Contractually

 

  • In small-scale problems, ADT requirements have a relatively finite scope
  • As software becomes more complex, we can no longer account for every single possible scenario
  • We restrict scope by defining the ​boundaries of the specification clearly
  • The client of the ADT and the ADT each have their own obligations and benefits
  • In business, when two parties interact with one another, they often write contracts​ to clarify obligations and expectations.

Defensive Programming

 

  • Consider every possible input that the ADT could be given
  • Map each possible input to a defined output
  • Addresses ​unforeseen circumstances, so that the software works no matter what is thrown at it
  • Useful in some scenarios, but too expensive to adopt everywhere, all the time:
    • ​Results in redundant checks (client and ADT perform checks)
    • Locating errors can be difficult to lack of clear demarcation of responsibilities
    • We may end up defending against errors that will never be encountered

Design by Contract

 

  • The software specification is clearly defined and documented
  • Results in simpler code and easier maintenance
  • "The spec, the whole spec, and nothing but the spec"
  • Correctness: Whether the implementation meets the specification (contract)
  • Correctness is enforced through unit testing
  • Benefits
    • Prevents redundant validation tasks
    • Given the preconditions are satisfied, the client can expect the postconditions
    • Makes development cleaner and faster
    • Responsibilities are clearly assigned between software components

Design by Contract

 

  • Three components to a software contract:
    • Preconditions: what does the contract expect?
      • If the precondition is true, cases outside of the precondition do not have to be handled
      • E.g. expected argument value mark >= 0 and marks <= 100
    • Postconditions: what does the contract guarantee?
      • Return value is guaranteed, provided the precondition is true
      • E.g. Correct return value representing a grade
    • Invariant: what does the contract maintain? (keep the same)
      • Some values must satisfy constraints, before and after execution (e.g. of a method)
      • E.g. A value of mark remains between 0 and 100

Contractual Correctness

 

  • All testing is determining contractual correctness; whether the implementation meets the specification
  • We have seen this in the form of dynamic verification, but it is also possible to have static verification as well (compilation, linting, type checking)
  • It is possible to mathematically prove correctness to a contract statically;
    • Some programming languages (Eiffel, Danny) offer native support for DbC and allow for static verification
    • Java does not have native support for DbC (there are various libraries available to support it)
  • Contracts must be:
    • Declarative (not imperative) and not include implementation details;
    • As much as possible: precise, formal and verifiable

Design by Contract: Example using Eiffel

Design by Contract: Examples in Java

Preconditions

 

  • A precondition is a condition or predicate that must always be true just prior to the execution of some section of code
  • If a precondition is violated, the behaviour of the section of code becomes undefined and thus may or may not carry out its intended work.
  • Often, preconditions are included in the documentation of the affected section of code.
  • Preconditions are sometimes tested using guards or assertions within the code itself, and some languages have specific syntactic constructions for testing .
  • In Design by Contract, a software element can assume that preconditions are satisfied, resulting in removal of redundant error checking code.

Preconditions: Examples

Postconditions

 

  • A post-condition is a condition or predicate that must always be true just after the execution of some section of code
  • The post-condition for any routine is a declaration of the properties which are guaranteed upon completion of the routine's execution[1] .
  • Often, preconditions are included in the documentation of the affected section of code.
  • Post-conditions are sometimes tested using guards or assertions within the code itself, and some languages have specific syntactic constructions for testing

  • In Design by Contract, the properties declared by the post-condition(s) are assured, provided the software element is called in a state in which its pre-condition(s) were true.

Invariants

 

  • The class invariant constrains the state (i.e. values of certain variables) stored in the object.
  • Class invariants are established during construction and constantly maintained between calls to public methods. Methods of the class must make sure that the class invariants are satisfied / preserved.
  • Within a method: code within a method may break invariants as long as the invariants are restored before a public method ends.
  • Class invariants help programmers to rely on a valid state, avoiding risk of inaccurate / invalid data. Also helps in locating errors during testing.

Preconditions in Inheritance


  • When we create subclasses, we must ensure that the inherited contract is still complied to
  • Liskov Substitution Principle: All subtypes must be substitutable for their base types - preserving correctess between super/subclasses
  • Preconditions
    • An implementation or redefinition (method overriding) of an inherited method must comply with the inherited contract for the method.
    • Preconditions may be weakened (relaxed) in a subclass, but it must comply with the inherited contract.
    • An implementation or redefinition may lessen the obligation of the client, but not increase it.

Preconditions in Inheritance

 

  • An implementation or redefinition (method overriding) of an inherited method must comply with the inherited contract for the method.
  • Preconditions may be weakened (relaxed) in a subclass, but it must comply with the inherited contract.
  • An implementation or redefinition may lessen the obligation of the client, but not increase it.

Postconditions in Inheritance

 

  • An implementation or redefinition (method overriding) of an inherited method must comply with the inherited contract for the method.
  • Post-conditions may be strengthened (more restricted) in a subclass, but it must comply with the inherited contract.
  • An implementation or redefinition (overridden method) may increase the benefits it provides to the client, but not decrease it.
  • For example:
    • the redefinition (overridden method) returns sorted set, offering more benefit to a client.

    • ​the original contract requires returning a set.

Class Invariants in Inheritance

 

  • Class invariants are inherited, that means,

    "the invariants of all the parents of a class apply to the class itself.” 

  • A subclass can access implementation data of the parents, however, must always satisfy the invariants of all the parents – preventing invalid states!

Safety & Security Considerations

 

  • Do we have to program defensively all the time to ensure our software is safe and secure?
  • We must think about the architecture - interactions between different software components
  • A question to ask: Who will be using your ADT (what client)?
    • A login form on a website: assume no preconditions;
    • A backend web server on an internal network: can assume some preconditions;
    • A public-facing API: assume no preconditions
    • Machine code executor: assumes that the code is valid and won't cause buffer overflows, etc. (lower-level contracts between the executor and OS ensure safety here)
    • A Java library used by programmers?
  • Checks are still done, just determine at what point they are done
  • Can't make any assumptions about user input

Exceptions

 

  • What if a client doesn't meet the preconditions?
  • In software, we can define different "flows" of interaction between the client and ADT
  • Three main types of flows:
    • Optimal - main success flow/use case
    • Optional - alternative success flow/use case
    • Exceptional - error flow/use case (e.g. invalid input was provided)
  • The exceptional behaviour is still defined, but doesn't meet the preconditions for an optimal flow
  • Exceptions are a way of informing the client that they haven't met the preconditions
  • Exceptions are part of the contract
    • In Java, whether an exception is checked or unchecked will affect compilation