2

According to Is this a violation of the Liskov Substitution Principle?, as I understand, the top answer currently says the code below is violating "Liskov Substitution Principle":

public class Task{
    private boolean isStarted;
    private boolean isClosed;

    public void close(){
       isClosed=true;
    }
}

public class ProjectTask extends Task{
    public void Close(){
        if (this.isStarted) 
            throw new Exception("Can't close a started ProjectTask");
        super.close();
   }
}

However, I'm not asking why it is violating "Liskov Substitution Principle", what I want to ask is, why would "Preconditions can't be strengthened in a subtype" and "Postconditions can't be weakened in a subtype" be 2 separate rules. Isn't they are the same? According to answer https://softwareengineering.stackexchange.com/a/187615/432039 that intercept the example above:

  • Preconditions can't be strengthened in a subtype: close() works on both started and non-started Task originally, but the subtype requires non-started Task, this is strengthened pre-conditions.

  • Postconditions can't be weakened in a subtype: The base class used to guarantee all Task would be closed after calling close(), but the subtype ProjectTask can be leave unclosed even after calling close() because of throwing exception, this is weakening post condition.

It seems that when "Preconditions strengthened" occurs, "Postconditions weakened" mostly also occurs, is it true? If not, what is the different between them? Why list 2 rules separately? Is there any case that postconditions are weakened but preconditions are not strengthened (or vice versa)?

1
  • 3
    Abraham Lincoln is dead because he was assassinated. Abraham Lincoln is also dead because he was born much longer ago than the longest-lived human to ever exist. Both are valid explanations about why Abraham Lincoln is dead, but that doesn't mean that the explanations themselves are the same. You're just dealing with an example where the practical result happens to be the same for more than one justifiable reason.
    – Flater
    Commented Oct 13, 2023 at 3:07

2 Answers 2

4

You are looking at a method that takes no parameters and returns nothing. Preconditions often involve the supplied parameters, and postconditions often involve the return value. If you default to preconditions involving the parameters, and postconditions involving the return value, it's more natural to express things with a clean return / exception divide.

A very similar definition "All Task would be closed after Close() returns." isn't weakened in ProjectTask, because Close() doesn't return.

2

In short

Strengthening the preconditions and weakening post conditions is not at all the same. These conditions have a different nature regardless of how you enforce them.

In many case, you could weaken preconditions or strengthen pre conditions independently, while both may result failed tests cases when reusing super-type tests for the sub-type.

More details

On LSP and contracts

LSP is about promises made in a contract:

  • preconditions need to be fulfilled before the an operation is performed. If you call the operation when they are not met, it doesn't mean that you have to throw exceptions. It just means that the promises of that operation (invariants, post conditions) are no longer guaranteed.
  • postconditions is what must be guaranteed after the operation is performed, provided the preconditions were initially met. If they are not fulfilled, there's something wrong in the code.

Contracts are not about exception (and vice-versa)

The pre-/post-conditions do not need to be implemented by the type and its subtype. Adding checks and exceptions (inside the the operations, or outside before/after calling them) is defensive programming.

Throwing may be the sign of a failed pre/post-condition. But an exception could as well be a promised behavior of the contract. For example the following contract would make your subtype LSP compliant without changing anything in the code:

Task::Close() ensures that the task is in a closed state if closing is possible or that an exception TaskCannotBeClosed is thrown.

In this case, in your Task no exception is thrown not because of a different contract but because simpler tasks can alway be closed and hence, no need to throw. And throwing for ProjectTask is no longer a strengthening of preconditions.

Example with independent pre and post conditions.

Take the following pseudo-code, which has unrelated pre and post conditions. Please ignore all the other design and style issues, it's just to illustrate the case:

class Transaction {
  ...
  Money sellProduct (Customer c, Product p, Quantity q) {
       
    if (c.age()<12) // precondition: Customer is aged 12+ 
      throw CustomerTooYoung;

    ... // do something 

    if (valueCashedIn < Money(0.00)) // postcondition: amount cashed-in is >=0
      throw InternalErrorOnValue;
    return valueCashedIn ; 
  }
  ...
}

You would have an LSP infringement by strengthening the precondition if you would have a TransactionDangerousGoods that has a precondition that customer is 18+. Absolutely no impact on postcondition.

You would have an LSP infringement on post-condition, if you would have a Takeback class as subtype of Transaction, if the tackeback would misuse the sellProduct() to return the reimbursed amount as negative value.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.