960 likes | 966 Views
Learn about Design by Contract principles in software engineering, based on the Eiffel programming language. Understand how contracts can organize communication between software elements and improve software specification. Discover the benefits and obligations of contracts and how they can be used to write robust and reliable software.
E N D
Software Engineering Design by Contract Software Engineering 2012 Department of Computer Science Ben-Gurion university Based on slides of: Mira Balaban Department of Computer Science Ben-Gurion university • R. Mitchell and J. McKim: Design by Contract by Example
Design By Contract • The term Design by Contract was coined by Bertrand Meyer while designing the Eiffel programming language within Eiffel Software company. • Eiffel implements the Design by Contract principles. • Bertrand Meyer won the 2006 ACM Software System Award for Eiffel The Eiffel Tower, built in 1887 for the 1889 World Fair, was completed on time and within budget, as will software projects written in Eiffel. Design by Contract
A contract • There are two parties • A Client - requests a service • A Supplier - supplies the service • A Contract is the agreement between the client and the supplier • Two major characteristics of a contract • Each party expects some benefits from the contract and is prepared to incur some obligations to obtain them • These benefits and obligations are documented in a contract document • Benefit of the client is the obligation of the supplier, and vice versa. Design by Contract
DbC – The idea and metaphor • Motivation: Organize communication between software elements • By organizing mutual obligations and benefits • Do it by a metaphor of • clients – request services from suppliers • suppliers – supply services Design by Contract
DbC – The metaphor realization • Obligations and benefits are specified using contracts • Write contracts for classes and methods • Methods: • Preconditions • Post-conditions • Classes: Invariants Design by Contract
What happens when a Contract Breaks? • If everyone does their job, there is no problem • If the precondition is not satisfied – • the Customer is wrong! (The client has a bug). • If the precondition is satisfied, but the postcondition is not • the Service is wrong (The service has a bug). • From the Client’s perspective, “true” is the best precondition. In general, weaker preconditions are better. • From the Server’s perspective, “false” is the best precondition. In general, stronger preconditions mean an easier job with the implementation. Design by Contract
DbC Nature • Dbc promotes software specification together with or prior to code writing • Writing contracts needs some principles and guidelines • The DbC principles tell us how to organize a class features (attributes, methods) • The contract of a class is its interface Design by Contract
Design by Contract, by Example Design by Contract
Notations features QUERIES attributes routines COMMANDS procedures functions creation other Design by Contract
Six Principles – the SIMPLE_STACK example Design by Contract
SIMPLE_STACK example – initial try • SIMPLE_STACK is a generic class, with type parameter G: Simple_stack of G. • Features: • Queries – functions; No side effect: • count(): Integer • is_empty(): Boolean • Initialization: • initialize() • Commands: • push(g:G) no return value. • pop(): out parameter g:G; no return value. G SIMPLE_STACK count() is_empty() initialize() push(g:G) pop() Design by Contract
Separate commands from queries (1) • Writing a contract for push: • Takes a parameter g. • Places g on the top of the stack. but pop does not return a value. Just removes. Redesign pop: New push contract: • push(g:G) • Purpose: Push g onto the top of the stack. • ensure: • g = pop ??? • pop(): G • purpose: Remove top item and return it. • push(g:G) • Purpose: Push g onto the top of the stack. • ensure: • g = pop Design by Contract
Separate commands from queries (2) • Serious problem: • Evaluation of the post-condition changes the stack! • Solution: Split pop into two operations: • Query: • Command: • push contract: • top(): G • purpose: return the item at the top of the stack. delete() purpose: deletes the item at the top of the stack. • push(g:G) • purpose: Push g onto the top of the stack. • ensure: • top = g Design by Contract
Separate commands from queries (3) • Standardize names: • Class: SIMPLE_STACK • Queries: • count(): Integer • purpose: No of items on the stack. • item(): G • purpose: The top item • is_empty(): Boolean • purpose: Is the stack empty? • Creation commands: • initialize() • purpose: Initialize a stack (new or old) to be empty. • Operations (other commands): • put(g:G) • purpose: Push g on top of the stack. • remove() • purpose: Removes the top item of the stack. Boolean queries have names that invite a yes/no question A standard name to add /delete item from any container class Design by Contract
Separate commands from queries (4) • Principle 1: Separate commands from queries. • Queries: Return a result. No side effects. Pure functions. • Commands: Might have side effects. No return value. • Some operations are a mixture: • pop() – removes the top item and returns it Separate into two pore primitive command and query of which it is mixed Design by Contract
Separate basic queries from derived queries (1) • Post-condition of is_empty: • Result is a contract built-in variable that holds the result that a function returns to its caller. • The effect of is_empty() is defined in terms of the count query. is_empty() is a derived query: It can be replaced by the test: count() = 0. Contracts of other features can be defined in terms of basic queries alone. No need to state the status of derived queries – no need to state in the post-condition of put() that is_empty() is false. state: count is increased, infer : is_empty=false from the contract of is_empty • is_empty(): Boolean • purpose: Is the stack empty? • ensure: • consistent_with_count: • Result = (count()=0) Design by Contract
Separate basic queries from derived queries (2) • Principle 2: Separate basic queries from derived queries. • Derived queries can be specified in terms of basic queries. • Principle 3: For each derived query, write a post-condition that defines the query in terms of basic queries. Design by Contract
Specify how commands affect basic queries (1) • Queries provide the interface of an object: • all information about an object is obtained by querying it. • Derived queries are defined in terms of basic queries (principle3). The effect of a command on an object should be specified in terms of basic queries. • For Simple_stack, define the effects of • put() • initialize() • remove() • in terms of the basic queries • count() • item() Design by Contract
Specify how commands affect basic queries (2) • The put command: • put() increases count by 1. • put() affects the top item. • “@pre” is borrowed from OCL (Object Constraint Language) • count()@pre refers to the value of the query count() when put() is called. • put(g:G) • Purpose: Push g onto the top of the stack. • ensure: • count_increased: • count() = count()@pre + 1 • g_on_top: • item() = g Design by Contract
Specify how commands affect basic queries (3) • The initialize() command: • Turns count() to 0: post-condition count() = 0. • Following initialization the stack includes no items. Therefore, no top item The query item() cannot be applied. • Implies a pre-condition for the query item: • Together, the 2 contracts, guarantee that applying item after initialize is illegal! • initialize() • purpose: Turns a stack (new or old) to be empty. • ensure: • stack_is_empty: • count() = 0 • item() : G • purpose: The top item on the stack. • require: • stack_is_not_empty: • count() > 0 Design by Contract
Specify how commands affect basic queries (4) • The remove() command: • Two effects: • Reduces the number of items by one. • Removes the top item, and uncovers the item pushed before the top one. • Pre-condition: Stack is not empty. • Problem: How to express the 2nd post-condition? The only queries are count and item. No way to refer to previous items. • remove() • purpose: The top item on the stack. • require: • stack_not_empty: • count() > 0 • ensure: • count_decreased: • count() = count()@pre - 1 Design by Contract
Specify how commands affect basic queries (5) • Rethink the basic queries. • Needed: A basic query that enables querying any item on the stack. • New basic query: • The new basic queries are: • count() • item_at() The contracts of all other queries and commands need to be redefined! • item_at(i : Integer) : G • purpose: The i-th item on the stack. • item_at(1) is the oldest; • item_at(count) is the youngest, and the stack top. • require: • i_large_enough: i > 0 • i_small_enough: i <= count Design by Contract
Specify how commands affect basic queries (6) • The item query is no longer a basic query • It turns into a derived query: item = item_at(count) • item() : G • purpose: The top of the stack. • require: • stack_not_empty: • count() > 0 • ensure: • consistent_with_item_at: • Result = item_at(count) Design by Contract
Specify how commands affect basic queries (7) • The put command revisited: • put(g : G) • purpose: Push g on top of the stack. • ensure: • count_increased: • count() = count()@pre + 1 • g_on_top: • item_at(count() ) = g Design by Contract
Specify how commands affect basic queries (8) • The initialize creation command revisited: • The precondition of item_at(i) together with count()=0 implies the second post-condition – this is summarized as a comment • initialize() • purpose: Initialize the stack to be empty. • ensure: • empty_stack: • count() = 0 • item_at is undefined: • --For item_at(i), i must be in the interval [1,count], which is empty, since count = 0 • -- there are no values of i for which item_at(i)_ is defined Design by Contract
Specify how commands affect basic queries (9) • The remove command revisited: • No need for another post-condition about the new top: • Once count is decreased, the new top of the stack item() - is item_at(count() ) • remove() • purpose: Remove the top item from the stack. • The new top is the one, put before the last one. • require: • stack_not_empty: count() > 0 • ensure: • count_decreased: • count() = count()@pre - 1 Design by Contract
Specify how commands affect basic queries (10) • Principle 4: For each command, specify its effect on basic queries. • Implies its effect on derived queries. • Usually: Avoid specifying queries that do not change. • Principle 5: For each query and command, determine a pre-condition. • Constrains clients. Design by Contract
summary • Every command specifies its effect on every basic query • Sometimes the specification is direct – an explicit assertion in the post condition • Sometimes the specification is indirect • Initialize specifies that count =0. The precondition of item_at() implies that there are no valid values of i for which item_at can be called • Indirectly initialize specifies the effect on item_at: it makes it invalid to call item_at() • All the derived queries have post conditions that specify their results in terms of the basic queries Design by Contract
Class invariants and class correctness • A class invariant is an assertion that holds for all instances (objects) of the class • A class invariant must be satisfied after creation of every instance of the class • The invariant must be preserved by every method of the class, i.e., if we assume that the invariant holds at the method entry it should hold at the method exit • We can think of the class invariant as conjunction added to the precondition and post-condition of each method in the class Design by Contract
Class invariants • Capture unchanging properties of a class objects by invariants • For the SIMPLE_STACK class, the non-negative value of count is an invariant: • Argument (proof): • For an initialized object: count() = 0 • Count() is decreased by remove, but it has the precondition: • count() > 0 • Principle 6: Write invariants to define unchanging properties of objects. • Provide a proof for each invariant. • A good collection of class invariants might involve all method contracts (in their proofs) • (if the invariant can be inferred from the contracts of the features it is redundant. • Include it ? • invariant: • count_is_never_negative: • count() >= 0 Design by Contract
The SIMPLE_STACK class interface (1) • Class SIMPLE_STACK(G) 1. Basic queries: 2. Derived queries: • count(): Integer • purpose: The number of items on the stack • item_at(i : Integer) : G • purpose: The i-th item on the stack. • item_at(1) is the oldest; item_at(count) is the youngest, and the stack top. • require: • i_large_enough: i > 0 • i_small_enough: i <= count() • item() : G • purpose: The top of the stack. • require: • stack_not_empty: • count() > 0 • ensure: • consistent_with_item_at: • Result = item_at( count () ) • is_empty: Boolean • purpose: Is the stack empty from items? Design by Contract
The SIMPLE_STACK class interface (2) • Class SIMPLE_STACK(G) … 3. Creation commands: • initialize() • purpose: Initialize the stack to be empty. • ensure: • empty_stack: count() = 0 • item_at is undefined: For item_at(i), i must be in the interval [1,count], which is empty, since count = 0 Design by Contract
The SIMPLE_STACK class interface (3) 4. Other commands: 5. Invariant: • put(g : G) • purpose: Push g on top of the stack. • ensure: • count_increased: count() = count()@pre + 1 • g_on_top: item_at(count() ) = g • remove • purpose: Remove the top item from the stack. The new top is the one, put before the last one. • require: • stack_not_empty: count() > 0 • ensure: • count_decreased: count() = count()@pre – 1 count_is-never_negative: count() >= 0 Design by Contract
The basic queries form a conceptual model • The two basic queries count and item_at give us a model of a stack object • Using this model we can say all there is to say about stacks: • What the stuck looks like when it is just been initialized: Count = 0 and there are no items since there is no i for which items_at(i) is valid. • What the effect of put(g) is : count is increased a g is item_at(count) • What the effect of remove is: count has decreased • What the result of is_empty is: The same as count=0 • What the result of item is: the same as item_at(count) Design by Contract
The basic queries form a conceptual model • We have devised a conceptual model of stacks. • Stacks have an ordered set of items • (item_at(1), item_at(2),item_at(3), and so on) • We know how many items there are • Count • The class designer devises the model and uses it as the basis of the contracts that specify the features of the class. • The programmer of the class can see the model and devise a suitable implementation model to represent it. 30 20 10 Count=3 item_at(3)=30 item_at(2)=20 item_at(1)=10 Design by Contract
The 6 principles • Separate commands from queries. • Separate basic queries from derived queries. • For each derived query, write a post-condition that defines the query in terms of basic queries. • For each command, specify its effect on basic queries. • For each query and command, determine a pre-condition. • Write invariants to define unchanging properties of objects. Design by Contract
Building Support for Contracts -Immutable (Value) Lists Design by Contract
Contracts for Immutable (Value) Lists • Contracts are written in expression languages, without side effects (functional languages). (Why? -- recall the Simple_stack class) • Contracts for clients of collection classes need to inspect the members of their collections. • Such contracts need side-effect protected operations on collections. • A conventional approach: Contracts that handle collection objects create them as value (immutable objects). • A client can hold a regular collection object, like a hash table, but create an immutable copy for the purpose of contract evaluation. • We start by defining the code for Immutable list Design by Contract
The IMMUTABLE_LIST class interface (1) • IMMUTABLE_LIST is a generic class, with type parameter G: IMMUTABLE_LIST of G. • Features: • Basic Queries: • Derived queries: • Head (): G • Purpose: The first item on the list • Tail (): IMMUTABLE_LIST(G) • Purpose: A new list, formed from the current list (termed ‘self’, minus the ‘head’ • is_empty(): Boolean • Purpose: Does the list contain no items? • count (): INTEGER • Purpose: The number of items in the list Design by Contract
The IMMUTABLE_LIST class interface (2) • Features: • derived Queries: • Creation commend: • … • cons(g:G): IMMUTABLE_LIST(G) • Purpose: A new list, formed from g as a head and the self list as a tail • is_equal( other: IMMUTABLE_LIST(G) ) : BOOLEAN • Purpose: Compare all ordered elements in self and in other • item( i:INTEGER ): G • Purpose: The i-th item in the list (starting from 1) • sublist( from_position:INTEGER, to_position:INTEGER ) : IMMUTABLE_LIST(G) • Purpose: A new list, formed from the self items at from_position to to_position • initialize () • Purpose: Initialize a list to be empty Design by Contract
Contracts of the basic queries • Basic Queries: • No post conditions: regular for basic queries • head (): G • Purpose: The first item on the list • require: • not_empty: notis_empty() • tail (): IMMUTABLE_LIST(G) • Purpose: A new list, formed from the self list, minus the ‘head’ • require: • not_empty: notis_empty() • is_empty: Boolean • Purpose: Does the list contain no items? Design by Contract
Contract of the creation command The creation command initialize empties a list (either new or old). • It takes no arguments – no precondition • Following its application: • The list should be empty. • head() and tail() should not be applicable • is_empty() should be true Arguments for the post conditions on head() and tail(): Their pre-condition require: notis_empty(), which is false following initialize() • Creation command: • initialize () • Purpose: Initialize a list to be empty • ensure: • empty: is_empty() Design by Contract
Contracts of the derived queries: count The post-condition of count():INTEGER • If the list is empty, count() is 0. • If the list is not empty, count() is the count() on the tail of the list + 1. • Derived query: count ():INTEGER Purpose: The number of items in the list ensure: count_zero_for_an_empty_list: is_empty() implies (Result = 0) count_for_a_non_empty_list: notis_empty() implies (Result = tail().count() + 1) Evaluated recursively Design by Contract
Contracts of the derived queries: cons The post-condition of cons(g:G ):IMMUTABLE_LIST): • The new list has, g as its head. • The new list has self as its tail. • The new list is not empty • Derived query: cons ( g:G ):IMMUTABLE_LIST) Purpose: A new list, formed from self and g, as its head ensure: not_empty: not Result.is_empty() head_is_g: Result.head() = g tail_is_self: Result.tail().is_equal(self) Design by Contract
Contracts of the derived queries: item • The pre-condition of item(i:INTEGER):G is that i is in the range [1..count()] • The post-condition of item(i:INTEGER):G: • The 1st item is the head • For i>=1, the i-th item is the (i-1)-th item of the tail • Derived query: The if operator evaluates its else component only if its predicate evaluates to false (or else – eiffel) item ( i:INTEGER ):G Purpose: The i-th item on the list require: i_large_enough: i >= 1 i_small_enough: i <= count() ensure: correct_item: if i=1 then Result = head() elseResult = tail().item(i-1) ( i=1 and Result = head() orelse Result = tail().item(i-1) Evaluated recursively Design by Contract
Contracts of the derived queries: item • The pre-condition of item(i:INTEGER):G is that i is in the range [1..count()] • The post-condition of item(i:INTEGER):G: • The 1st item is the head • For i>=1, the i-th item is the (i-1)-th item of the tail • Derived query: The if operator evaluates its else component only if its predicate evaluates to false (or else – eiffel) Evaluation of postcondition for the list [5,4,7,2] with i=3 [5,4,7,2].item(3)= [4,7,2].item(2)= --i.e. item(i-1) of tail [7,2].item(1)= --again item(i-1) of tail 7 --this time i=1 so the result is the head of the list item ( i:INTEGER ):G Purpose: The i-th item on the list require: i_large_enough: i >= 1 i_small_enough: i <= count() ensure: correct_item: if i=1 then Result = head() elseResult = tail().item(i-1) ( i=1 and Result = head() orelse Result = tail().item(i-1) Evaluated recursively Design by Contract
Contracts of the derived queries: is_equal • The pre-condition of is_equal(other:IMMUTABLE_LIST):BOOLEAN is that the argument list exists (e.g., is not a null pointer). • The post-condition of is_equal(other:IMMUTABLE_LIST):BOOLEAN : • Both lists might be empty • Both lists are not empty, and their heads and tails are equal. Derived query: or_else and and_then are Eiffel’s operators for optimized evaluation of or and and is_equal(other:IMMUTABLE_LIST):BOOLEAN Purpose: Are the 2 lists, self and other equal in elements and order? require: other_exists: other /= null ensure: same_content: Result = ( is_empty()andother.is_empty() ) or_else ( ( (notis_empty()) and (notother.is_empty()) ) and then ( head() = other.head() and tail().is_equal(other.tail()) ) ) Evaluated recursively Design by Contract
Contracts of the derived queries: sublist sublist( from_position:INTEGER, to_position:INTEGER ):IMMUTABLE_LIST: • The pre-condition requires that the positions are in the [1..count()] range, and are consistent: from_position is <= to_position+1 (to enable extracting an empty sublist). • The post-condition: • Ifto_positionis smaller than from_position the extracted sublist is empty • Otherwise, the extracted list consists of the list elements at positions from_position to to_position • A new list is constructed The contract: Design by Contract
Contracts of the derived queries: sublist sublist( from_position:INTEGER, to_position:INTEGER ):IMMUTABLE_LIST Purpose: A new list, formed from the items at positions from_position to to_position require: from_position_large_enough: from_position>= 1 from_position_small_enough: from_position<= to_position+ 1 to_position_small_enough: to_position<= count() ensure: is_empty_is _consistent_with_from_and_to_position: Result.is_empty() = ( from_position>to_position ) result_head _is_at _from_position_in_current: (from_position <= to_position) implies (Result.head() = item( from_position ) ) result_tail _is_correct_sublist_within_current: (from_position <= to_position) implies (Result.tail().is_equal( sublist( from_position+1, to_position) ) ) To allow zero sized sublist [] Design by Contract
Assertion checking modes • In full assertion checking mode, pre-conditions, post-conditions and invariants are always checked. • For expensive contracts like those of the IMMUTABLE_LIST class, full contract evaluation is a performance penalty. • Therefore, there are different modes of contract evaluation. • Testing mode: Full evaluation • Working mode: Only pre-condition evaluation Argument: Once the class is believed to be bug-free, the post-conditions and invariants are believed to hold as stated. checking pre-conditions is needed to protect against irresponsible clients Design by Contract