(see
OOSC chapter 1)
1. Correctness
Exactly performs its tasks, according to
specification
2. Robustness
Function even in abnormal conditions
3. Extendability
Can be adapted to change in specifications
4. Reusability
Can be reused in new applications
5. Compatibility
Can be combined with other products
Reliability = Correctness + Robustness |
Today: Focus on how
to writing reliable software
- systems that work.
Aside: These are "software engineering" issues. They are only indirectly helped by O-O. They have been addressed by non O-O languages such as Ada.
class Account { int balance; public void withdraw(amount: int) { balance = balance - amount; } }
Question: What should happen if the balance was 200 and amount was 1000?
withdrawal_done
attribute in class?
What to do? Observation:
Your final product is not a withdraw
function, or even an Account
class. It is a system which uses the withdraw
function.
Think of the
withdraw function of the
Account
class as the service provider and the user of it as
the client.
Whose responsibility is it to check that
amount
<= balance ?
It is the responsibility of the client to ensure that data is correct. |
(This idea of
viewing a routine as a contract is due to Bertrand Meyer, author of
Eiffel.
Rebecca Wirfs-Brock has a similar idea in her
responsibility driven approach.)
Reference: B. Meyer "Applying Design by Contract" Computer, Oct 1992, pp 40 - 51 (see also OOSC chapter 11)
If you can't do something yourself, you may decide to get someone else to do it. You enter into a contract. Each party to the contract expects benefits, and is prepared to incur obligations. These benefits and obligations are spelled out in a contract document.
Party |
Obligations |
Benefits |
---|---|---|
Client |
Provide package within
weight and dimension limits. |
Package delivered same day |
Supplier |
Deliver package same day |
Don't have to worry about heavy, large or unpaid packages |
An obligation on one party is usually a benefit to another.
But the obligation also provides a benefit in that all the requirements are spelled out - there are no hidden clauses.
Obligations |
Benefits |
|
---|---|---|
Student |
assignments handed in on time |
prompt feedback |
Tutor |
assignments marked by next tutorial |
weekend to mark assignments all assignments marked in one block |
Obligations |
Benefits |
|
---|---|---|
Client |
only call withdraw with |
the balance is correctly adjusted |
Server |
ensure that the withdrawal is done according to business rules |
don't have to worry whether there are enough funds in account |
It is necessary to
specify the relationship between the client and the supplier as
precisely as possible.
This is the "no hidden clause"
principle.
The mechanisms for expressing such conditions are
called assertions.
Preconditions
express the requirements that any call must satisfy if it is to be
correct.
Postconditions express properties that are ensured in
return by the execution of the call.
Each routine has pre and post conditions which bind the routine and its callers. Can be viewed as a contract between server and client.
precondition: | binds the caller |
postcondition: | binds the routine |
The issue here is software quality In particular two concerns
The issue of
correctness is addressed by assertions
(pre and post conditions,
class invariants, etc).
The issue of robustness is addressed by exception handling
A class is correct when it does everything it is supposed to do and nothing else. |
Every component must
have a well defined task. |
Expressing of pre and post conditions varies from formal specification languages such as Object Z or formal algebra to informal English statements. Which approach you use will depend on the domain in which the model is developed and the experience of the modeller. Java + iContract uses Boolean expressions, which can be tested.
Examples: (informal English)
Object |
Routine |
Precondition |
---|---|---|
|
sqrt |
argument non-negative |
stack |
pop |
stack not empty |
push |
stack not full |
Object |
Routine |
Postcondition |
---|---|---|
|
sqrt |
(result)2 reasonably close to argument |
stack |
push E |
E is top of stack |
table |
insert E with key K |
table has E with key
K |
account |
deposit D |
balance greater by D |
Expressing
pre and post conditions as assertions
(Boolean
expressions)
x non-negative |
|
(result)2
close to x |
|
stack can't be empty |
|
size is 1 greater |
num_elts = old num_elts + 1 |
E is the top of the
stack |
top = E |
balance is D greater |
balance = old balance + D |
Java does not currently support contracts. There are tools such as iContract which add contract support to Java. iContract puts pre- and post-conditions in the documentation comments for methods.
Requirements should appear as preconditions or in the body of the code, but not both.
/**
* @pre ! isEmpty()
*/
int pop() {
...
}
OR
int pop() {
if (isEmpty()) {
...
} else {
...
}
}
This focuses on whose responsibility it is to check.
It is the opposite of "defensive programming" - it avoids redundant tests.
No absolute rule.
Demanding style: precondition is strong, with
responsibility on clients.
Tolerant style: more
obligation on the routine.
Experience in writing Eiffel libraries is that demanding style works well. Each routine has a well-defined task which it does well, as opposed to trying to handle every imaginable case.
Ultimate test is to adopt the solution which achieves the simplest architecture.
Setting error flags seems appropriate for databases.
"Demanding" pop
/**
* Remove top element
* @pre ! isEmpty()
*/
int pop() {
...
}
"Tolerant" pop
/**
* Remove top element if it exists
* else set error flag.
* No precondition (precondn = True)
*/
int pop() {
if( isEmpty()) {
error = true;
} else {
error = false;
...
}
}
Assertions are specifications, not instructions.
They describe the conditions under which the software elements will work, and the conditions they will satisfy in return.
Any runtime violation of an assertion is not a special case, but a manifestation of a bug:
pop(push(x,s)) = s
Relate to the global properties of a class - they tell us what is common to all legal states of the object.
number of elements >= 0
number of elements <= max elements
empty = (number of elements = 0)
At the analysis/specification level they correspond to 'business rules'.
The bank balance must be positive
(or >= overdraft limit)
No more than 5 withdrawals per month
There must be between 3 and 8 members in a committee
Class invariants which summarise information regarding the interaction of two or more features of a class are especially helpful.
At creation of an account, balance
>= overdraft limit
To withdraw amount
requires balance - amount >=
overdraft limit
So we always have
balance >= overdraft limit
This makes it a class invariant
On the state of an object
balance >= 0
count <= max
This is the most common
On relationships between features
For example, postconditions on reserving a book may be
reserved
not reserveList.isEmpty()
if there is only 1
reserver, and that reserver borrows the book, the postconditions are
not reserved
reserveList.isEmpty()
where there is a link like this, you can pin it down as a class invariant
reserved = ! reserveList.isEmpty()
then the postconditions above simplify to
reserved
and
not reserved
respectively
Obligations |
Benefits |
|
---|---|---|
Client |
only add a person if there are fewer than 8 members |
person is added to
cttee, |
Server (cttee) |
person is member of
cttee |
don't need to worry about whether the committee is full |
Precondition:number of members is less than maximum
Postconditions:
/**
* @invariant count <= MAX_COUNT
* @invariant count >= MIN_COUNT
*/
class Committee {
List memberList;
int count;
final int MAX_COUNT = 8;
final int MIN_COUNT = 3;
/**
* pre count < MAX_COUNT
* post memberList.has(p) &&
* count = count@pre + 1
*/
void addMember(Person p) {
...
}
}
Data does have to be checked, but this approach focuses on whose responsibility it is.
Data should be checked at input time.
Programs should be simple. Complexity is the single most important enemy of software quality.
Redundant checking is not harmless
Should you trust
the client ?
Maybe not, but if you can't trust them to test before
why trust them to test after?
What to do if a condition is not met is the issue of robustness, addressed by exception-handling.
Preconditions must be guaranteed, but that may not mean testing. The context may be enough to guarantee the precondition.
while event queue is not empty
get next event // no testing necessary