960 likes | 971 Views
Software Engineering Design by Contract. Software Engineering 2011 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.
E N D
Software Engineering Design by Contract Software Engineering 2011 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 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 • Initializespecifies 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 <= 0 • 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) 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: 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: Evaluation of postconditionfor 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 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 Contraor_else and and_then ct
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_lsmall_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