Design by Contract (DbC)
- also known as contract programming, programming by contract and design-by-contract programming
- it is an approach for designing software
- it is basically a software correctness methodology
- It prescribes that software designers should define formal, precise and verifiable interface specifications for software components
- which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants
- These specifications are referred to as
contracts
, in accordance with a conceptual metaphor with the conditions and obligations of business contracts
Correctness
the first term we need to understand is
system correctness
correctness can be applied to the
function specification
, but not to thefunction code
- we can only say if function is correct or incorrect when talking about expected results
function specification is fully correct when both precondition and postcondition are met
- it is the only case when we can say that the function is correct when we are talking about its results and both pre- and postconditions are met
example 1
- This function by itself is neither correct nor incorrect
- correctness can be applied when we talk about expected results
- This function is correct if we say that "Returned value is half the size of the argument"
- but it is not correct if we say that "Returned value should be positive"
- because there are no guarantees that the function will not receive a negative number as the argument
- The first triple is correct as before the operation
x ^ 2
precondition is met and x is equal to 5- then after the operation postcondition (x is greater than 0) also will be met (subject to correct implementation of integer arithmetic)
- In this example postcondition is not the strongest one, the strongest possible postcondition for this precondition is
{x = 25}
and the weakest one is{x > 0}
- We can always create a new triple from existing one making precondition and postcondition weaker or stronger
- The concept of
stronger
andweaker
came from logic - It is said that the condition
X > 5
is stronger thanX > 0
X > 0
is weaker thanX > 5
if the fulfillment ofX > 5
implies the fulfillment ofX > 0
, but they are not equivalent- For example,
X > 5
is stronger thanX > 0
- since if
X > 5
is fulfilled,X > 0
is fulfilled as well (after all, if x is greater than 5, then, naturally, x is greater than 0), and they are not equivalent
- since if
- For example,
// Example 1
const doSomething = (x) => {
return x / 2;
};
// Example 2
// { x = 5 } x = x ˆ 2 { x > 0 } => the weakest postcondition
// Example 3
// { x = 5 } x = x ˆ 2 { x = 25 } => the strongest postconditionexample 2
Each function has strict semantic properties that reflect what the function does, regardless of how it does it
preconditions
- define properties that must be met every time before function is executed
- The precondition binds the calling code
- the conditions are defined under which the program call by the client is legitimate
- e.g.
x > 0
for theSqrt
function orcount ! = 0
for thePop
function of theStack
class
- e.g.
- In this case, the client's obligations benefit the provider class, since the class performing the operation does not need to worry about what to do if a precondition is violated
- return a default value or error code, throw an exception, save information about the error to the I/O stream, or interrupt the program execution
- the conditions are defined under which the program call by the client is legitimate
postconditions
- properties that must be met after its execution
- The postcondition binds the class
- the conditions that must be met upon completion of the operation are determined
- the
Stack
class must provide an increase in the number of elements by 1 after thepush
function is executed
- the
- the client's benefit (the result of performing the function) turns into the supplier's obligations (it can no longer fail to fulfill its obligations, since they are spelled out in the contract)
- the conditions that must be met upon completion of the operation are determined
class Stack<T> {
private count = 0;
private maxAmount = 15;
public push(e: T) {
console.assert(this.count < this.maxAmount, "Stack not overflown");
const previousCount = this.count;
// method implementation
console.assert(this.count === previousCount + 1, "One more item");
}
public pop(): T {
console.assert(this.count !== 0, "Stack is empty");
// method implementation
console.assert(result !== null, "Result is not null");
console.assert(this.count === previousCount - 1, "One less item");
return result;
}
}
Preconditions and Postconditions for Inheritance
Condition P1 is stronger than P2 and P2 is weaker than P1 when
- the fulfillment of P1 implies the fulfillment of P2, and they are not equivalent correct
- This is the most correct way to say which condition is stronger
Liskov substitution principle
is the most closely related to preconditions and postconditions- this principle says that you should be able to replace the base class with its subclass and the program should work correctly with it, or in other words it tells that pre- and postconditions should be met for the subclass
example
- C class which contains link to a B class
- Due to dynamic linking D class (or any other B subclass) can be used instead of B class after the start of program execution
- B class has a public foo function with a precondition of
x > 5
(pre_b) and a postconditionresult > 0
(post_b)- By checking the precondition, class C fulfills its part of the contract and can expect class B (or one of its subclasses) to fulfill the contract
- According to the Liskov substitution principle, the behavior of the given code fragment should not change if we substitute any B subclass
- Assuming that the function foo in class D starts to require more (contains a stronger precondition like
x > 10
), and guarantees less (contains a weaker postcondition likex > -5
)
- In this case, although the client of class B fulfills its part of the contract and provides an input value to the function foo that satisfies the precondition, it may not get the expected result
- Strengthening the precondition means that the data correct for the base class will become incorrect for its subclass (in this example, it can be the value x equal to 6)
- weakening the postcondition means that the result that the client of the base class expects may not be returned by the subclass (in this example, this could be the result of the function Foo equal to -1)
- Hence, we can conclude that when overriding methods, the precondition can be replaced only by
- an equal or weaker one (require less)
- and a postcondition
- only equal to it or stronger (guarantees more)
- The new version of the method should not reject calls allowed in the original
- it should, at a minimum, provide guarantees equivalent to those of the original version
- It is free, although not obligated, to allow more calls or provide stronger guarantees
class B {
public foo(x: number): number {
console.assert(x > 5, "x > 5");
//method implementation
console.assert(result, "result > 0");
return result;
}
}
class C {
private b: B;
public bar(x: number) {
if (x > 5) {
const result = this.b.foo(x);
console.assert(result > 0, "result > 0");
}
}
}
Covariance and Contravariance
when replacing the base class with its subclass
- the input values of subclass for its methods must be
contravariant
- that is, the precondition must be the same or weaker
- when the input values are contravariant you can be sure that subclass will always have the same or weaker preconditions and can be used instead of base class
- the output values of subclass must be
covariant
, that is, the same or stronger- when the output values are covariant or in other words when postcondition is the same or stronger you can be sure that the calling code will always get expected result
- the input values of subclass for its methods must be
example 1
- have inheritance structure built of three classes
- Locality as a base class and City and NewYork as more specific classes
- Function with name covariance accepts City instances as an argument
- but it also can accept NewYork instances as the more specific subclass
- at the same time it can not accept City superclass as the argument
- Locality because covariance will be violated in such case
- Everything is vice versa for the contravariance function
- it can accept City and its supertype as the argument but cannot accept City subclass
class Locality {}
class City extends Locality {}
class NewYork extends City {}
function covariance(city: City): void {}
covariance(new Locality()); // error... does not support supertype
covariance(new City()); // ok support exact type
covariance(new NewYork()); // ok support subtype
function contravariance(city: City): void {}
contravariance(new Locality()); // ok support supertype
contravariance(new City()); // ok support exact type
contravariance(new NewYork()); // error... does not support subtype- have inheritance structure built of three classes
example 2
- to understand how contravariance works on input values
- it also have an inheritance hierarchy of three classes, the base Destination, its USADestination subclass and the even more specific TexasDestionation class
- If we want to create a ShippingCalculator class that will accept USADestination as input and return the Price to us, and then create its InternationalShippingCalculator subclass which will already accept any Destination as input
- then this will be correct since the contravariance of the input values is observed, and we can replace the ShippingCalculator with InternationalShippingCalculator, and it will work
- But if after that we want to extend the functionality of the ShippingCalculator by creating its TexasShippingCalculator subclass which will only accept TexasDestionation as input
- then in this case the contravariance is not observed, since the input value is more specific, and in this case we cannot replace the ShippingCalculator on TexasShippingCalculator while keeping the system working
class Price {}
class Destination {}
class USADestination extends Destination {}
class TexasDestination extends USADestination {}
class ShippingCalculator {
public getRate(destination: USADestination): Price {
return new Price();
}
}
class InternationalShippingCalculator extends ShippingCalculator {
// ok - contravariance on input value
public getRate(destination: Destination): Price {
return new Price();
}
}
class TexasShippingCalculator extends ShippingCalculator {
// wrong - covariance on input value
public getRate(destination: TexasDestination): Price {
return new Price();
}
}example 3
- how to meet contravariance for the output values
- have the base Animal class and its three subclasses, while the Snake and Wolf classes are its direct subclasses, and the CanisLupus class is a Wolf subclass
- If we want to create a ZooCage class that will return the contents of the cage and the expected output value will be the Wolf class
- then to maintain covariance in the CanisLupusCage class, we need to return the Wolf or CanisLupus class
- If we create a Terrarium class with an output value of the Animal class
- then the covariance will not be met, since the Animal class is a superclass of the Wolf class
class Animal {
public feed(food): void {
console.log("yumm!");
}
public walk(): void {
throw new Error("Not implemented");
}
}
class Snake extends Animal {
public crawl(): void {
console.log("crawling");
}
}
class Wolf extends Animal {
public walk(): void {
console.log("walking");
}
}
class CanisLupus extends Wolf {}
class ZooCage {
public getContent(): Wolf {
return new Wolf();
}
}
class CanisLupusCage extends ZooCage {
// ok - returns subtype
public getContent(): CanisLupus {
return new CanisLupus();
}
}
class Terrarium extends ZooCage {
// wrong - returns supertype
public getContent(): Animal {
return new Snake();
}
}