开发者

When to add a precondition and when to (only) throw an exception?

开发者 https://www.devze.com 2023-02-27 07:13 出处:网络
I am learning about preconditions and when to use them. I have been told that the precondition @pre fileName must be the name of a valid file

I am learning about preconditions and when to use them. I have been told that the precondition

@pre fileName must be the name of a valid file

does not suit in the following code:

/**
Creates a new FileReader, given the name of file to read from.
@param fileName- the name of file to read from
@throw FileNotFoundException - if the named file does not exist,
is a directory rather than a regular file, or for some other reason cannot
be opened for reading.
*/
public FileReader readFile(String fileName) throws FileNotFoundException {
. . .
}//readFile

Why is this?

Edit: Another example

We are assuming that the following, as an example, is done the "right" way. Note the IllegalArgumentException and the precondition. Note how the behavior is well defined, and how the throws declaration is made even though a precondition is set. Most importantly, notice how it doesn't contain a precondition for the NullPointerException. Once again, why doesn't it?

/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @pre start <= end
* @post The time span of the returned period is positive.
* @throws开发者_开发技巧 IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) f

Are these examples avoiding use of extra preconditions? One could argue that if we are avoiding preconditions, then why have them at all? That is, why not replace all preconditions with @throws declarations (if avoiding them is what is done here)?


Wikipedia defines precondition as:

In computer programming, a precondition is a condition or predicate that must always be true just prior to the execution of some section of code or before an operation in a formal specification.

If a precondition is violated, the effect of the section of code becomes undefined and thus may or may not carry out its intended work.

In your example, the effect of the method if the filename is invalid is defined (it must throw a FileNotFoundException).

Put differently, if file being valid were a precondition we'd know that it was always valid, and the part of the contract that mandates an exception is thrown it is wasn't would never apply. Unreachable specification cases are a code smell just like unreachable code.

Edit:

If I have some preconditions, and can provide defined behavior for these conditions, wouldn't it be better if I did so?

Of course, but then it's no longer a precondition as defined by Hoare. Formally speaking, that a method has precondition pre and postcondition post means that for each execution of the method that started in state prestate and ended in state poststate

pre(prestate) ==> post(poststate)

If the left hand side of the implication is false, this is trivially true irrespective of what poststate is, i.e. the method will satisfy its contract irrespective of what it does, i.e. the method's behaviour is undefined.

Now, fast forward to modern times, where methods can throw exceptions. The usual way to model exceptions is to treat them as special return values, i.e. whether an exception occurred is part of the postcondition.

The exception is not really unreachable though, is it?

If the throws clause is part of the postcondtion, you have something like:

pre(prestate) ==> (pre(prestate) and return_valid) or (not pre(prestate) and throws_ exception)

which is logically equivalent to

pre(prestate) ==> (pre(prestate) and return_valid)

that is, it doesn't matter whether you write that throws clause, which is why I called that specification case unreachable.

I would say that an exception rather works as a supplement to the precondition to inform the user of what's going to happen if he/she breaks the contract.

No; the throws clause is part of the contract, and as such carries no weight if the contract is broken.

Of course it is possible to define that @throws clauses need to be satisfied irrespective of the precondition, but is that useful? Consider:

@pre foo != null
@throws IllegalStateException if foo.active

Must the exception be thrown if foo is null? In the classic definition, it is undefined, because we assume nobody will pass null for foo. In your definition, we have to explicitly repeat that in every throws clause:

@pre foo != null
@throws NullPointerException if foo == null
@throws IllegalStateException if foo != null && foo.active

If I know no reasonable programmer is going to pass null to that method, why should I be forced to specify that case in my specification? What benefit does it have to describe behavior that is not useful to the caller? (If the caller wants to know whether foo is null, he can check it himself rather than calling our method and catching the NullPointerException!).


Ok, so this is what I've found out:

Background

Based on the following principles, as described in Bertrand Meyer's book Object Oriented Software Construction:

"Non-Redundancy principle Under no circumstances shall the body of a routine ever test for the routine’s precondition." - Bertrand Meyer

"Precondition Availability rule Every feature appearing in the precondition of a routine must be available to every client to which the routine is available." - Bertrand Meyer

, these two points answer this question:

  1. For preconditions to be useful, the client (user of method) has to be able to test them.
  2. The server should never test the precondition, because this will add complexity to the system. Although, assertions are turned on to do this testing when debugging the system.

More on when, why, and how to use preconditions:

"Central to Design by Contract is the idea, expressed as the Non-Redundancy principle, that for any consistency condition that could jeopardize a routine’s proper functioning you should assign enforcement of this condition to only one of the two partners in the contract. Which one? In each case you have two possibilities: • Either you assign the responsibility to clients, in which case the condition will appear as part of the routine’s precondition. • Or you appoint the supplier, in which case the condition will appear in a conditional instruction of the form if condition then ..., or an equivalent control structure, in the routine’s body.

We can call the first attitude demanding and the second one tolerant." - Bertrand Meyer

So a precondition should only exist if it is decided that the client holds the responsibility. Since the server should not test the precondition, the behaviour becomes undefined (as also stated on Wikipedia).

Answers

  • The first point answers the first example.
  • As for the second example, it is probably not done the right way. This is because the first @throws declaration implies that the method has (other than an assertion) tested the precondition. This violates the second point.

As for the null pointer; this shows that the null pointer responsibility is assigned to the server. That is, using a "tolerant attitude", as opposed to a "demanding attitude". This is perfectly OK. If one chose to implement a demanding attitude, one would remove the throws declaration (but more importantly; not test for it), and add a precondition declaration (and perhaps an assertion).


I think that design by contract idea (I don't use it myself yet) and pre/post conditions are intended to guarantee specific conditions incoming and outgoing from the method. Particularly the compiler (in this case, theoretically as Java does not have this built-in) would need to be able to validate the contract conditions. In the case of your file precondition, this cannot be done since the file is an external resource, the class may move and the same file may not be there, etc. How can a compiler (or pre-processor) guarantee such a contract?

On the other hand, if you are just using it for comments then it would at least show the other developer what you expect but you must still expect too that there will be exceptions when the file does not exit.

I think that it "does not suit" the method in the formal sense of design by contract because it cannot be validated for even one case. That is, you may give valid file names in one environment but that may not be valid in other environment external to your program.

On the other hand, the date example, the pre and post conditions can be validated in the caller context as they are not influenced by external environmental setup that the method itself has no control over.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号