Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this precondition a violation of the Liskov Substitution Principle

I have 3 classes, Account, CappedAccount, UserAccount,

CappedAccount, and UserAccount both extend Account.

Account contains the following:

abstract class Account {
   ...
   /**
   * Attempts to add money to account.
   */
   public void add(double amount) {
      balance += amount;
   }
}

CappedAccount overrides this behavior:

public class CappedAccount extends Account {
   ...
   @Override
   public void add(double amount) {
      if (balance + amount > cap) { // New Precondition
         return;
      }
      balance += amount;
   }
}

UserAccount doesn't override any methods from Account, so it doesn't need to be stated.

My question is, does CappedAccount#add violate LSP, and if it does, how can I design it to comply with LSP.

For example, does add() in CappedAccount count as "strengthening preconditions"?

like image 692
Suneet Tipirneni Avatar asked May 13 '20 17:05

Suneet Tipirneni


People also ask

Which is a violation of the Liskov Substitution Principle?

A very common violation of this principle is the partial implementation of interfaces or base class functionality, leaving unimplemented methods or properties to throw an exception (e.g. NotImplementedException).

Which of the following are violations of LSP?

LSP violations symptomsDerivates that override a method of the base class method to give it completely new behaviour. Derivates that override a method of the superclass by an empty method. Derivates that document that certain methods inherited from the superclass should not be called by clients.

Which of the following from the solid principles is a typical violation of LSP?

From Liskov Substitution Principle: A typical example that violates LSP is a Square class that derives from a Rectangle class, assuming getter and setter methods exist for both width and height. The Square class always assumes that the width is equal with the height.

What is the best example of Liskov Substitution Principle?

The classic example of the inheritance technique causing problems is the circle-elipse problem (a.k.a the rectangle-square problem) which is a is a violation of the Liskov substitution principle. A good example here is that of a bird and a penguin; I will call this dove-penguin problem.

Is this a violation of Liskov substitution principle?

Yes, it is a violation of the LSP. Liskov Substitution Principle requires that. Preconditions cannot be strengthened in a subtype. Postconditions cannot be weakened in a subtype. Invariants of the supertype must be preserved in a subtype. History constraint (the "history rule").

Why deriving myArray from MyCollection is a Liskov violation?

Deriving MyArray from MyCollection when MyCollection provides Add method is a violation of Liskov Substitution Principle, because arrays have fixed size and not allow adding new items. We can't safely substitute collection instance with an array in all places.

What happens if you violate the LSP principle?

This was an example for the "preconditions cannot be strengthened in a subtype" part in the wiki article. So to sum up, violating LSP will probably cause errors in your code at some point. LSP says that ''Objects should be replaceable by their subtypes''. On the other hand, this principle points to

When subclassing a class the precondition and postcondition can only be weakened?

When subclassing a class, the precondition may only be weakened ( S.f accepts more than T.f) (a). The postcondition defines what is a valid result. When subclassing a class, the postcondition may only be strengthened ( S.f provides more than T.f) (b). The invariant defines what is a valid internal state.


Video Answer


2 Answers

It's important to remember the LSP covers both syntax and semantics. It covers both what the method is coded to do, and what the method is documented to do. This means vague documentation can make it difficult to apply the LSP.

How do you interpret this?

Attempts to add money to account.

It's clear the add() method is not guaranteed to add money to the account; so the fact that CappedAccount.add() may not actually add money seems acceptable. But there is no documentation of what should be expected when an attempt to add money fails. Since that use case is undocumented, "do nothing" seems like an acceptable behavior, and therefore we have no LSP violation.

To be on the safe side, I would amend the documentation to define expected behavior for a failed add() i.e. explicitly define the post-condition. Since the LSP covers both syntax and semantics, you can fix a violation by modifying either one.

like image 63
jaco0646 Avatar answered Oct 17 '22 12:10

jaco0646


TLDR;

if (balance + amount > cap) {
    return;
}

is not a precondition but an invariant, hence not a violation (on his own) of the Liskov Substition Principle.

Now, the actual answer.

A real precondition would be (pseudo code):

[requires] balance + amount <= cap

You should be able to enforce this precondition, that is check the condtion and raise an error if it is not met. If you do enforce the precondition, you'll see that the LSP is violated:

Account a = new Account(); // suppose it is not abstract
a.add(1000); // ok

Account a = new CappedAccount(100); // balance = 0, cap = 100
a.add(1000); // raise an error !

The subtype should behave like its supertype (see below).

The only way to "strengthen" the precondition is to strenghten the invariant. Because the invariant should be true before and after each method call. The LSP is not violated (on his own) by a strengthened invariant, because the invariant is given for free before the method call: it was true at the initialisation, hence true before the first method call. Because it's an invariant, it is true after the first method call. And step by step, is always true before the next method call (this is a mathematicual induction...).

class CappedAccount extends Account {
    [invariant] balance <= cap
}

The invariant should be true before and after the method call:

@Override
public void add(double amount) {
    assert balance <= cap;
    // code
    assert balance <= cap;
}

How would you implement that in the add method? You have some options. This one is ok:

@Override
public void add(double amount) {
    assert balance <= cap;
    if (balance + amount <= cap) {
        balance += cap;
    }
    assert balance <= cap;
}

Hey, but that's exactly what you did! (There is a slight difference: this one has one exit to check the invariant.)

This one too, but the semantic is different:

@Override
public void add(double amount) {
    assert balance <= cap;
    if (balance + amount > cap) {
        balance = cap;
    } else {
        balance += cap;
    }
    assert balance <= cap;
}

This one too but the semantic is absurd (or a closed account?):

@Override
public void add(double amount) {
    assert balance <= cap;
    // do nothing
    assert balance <= cap;
}

Okay, you added an invariant, not a precondition, and that's why the LSP is not violated. End of the answer.


But... this is not satisfying: add "attempts to add money to account". I would like to know if it was a success!! Let's try this in the base class:

/**
* Attempts to add money to account.
* @param amount  the amount of money
* @return True if the money was added.
*/
public boolean add(double amount) {
    [requires] amount >= 0
    [ensures] balance = (result && balance == old balance + amount) || (!result && balance == old balance)
}

And the implementation, with the invariant:

/**
* Attempts to add money to account.
* @param amount  the amount of money
* @return True is the money was added.
*/
public boolean add(double amount) {
    assert balance <= cap;
    assert amount >= 0;
    double old_balance = balance; // snapshot of the initial state
    bool result;
    if (balance + amount <= cap) {
        balance += cap;
        result = true;
    } else {
        result = false;
    }
    assert (result && balance == old balance + amount) || (!result && balance == old balance)
    assert balance <= cap;
    return result;
}

Of course, nobody writes code like that, unless you use Eiffel (that might be a good idea), but you see the idea. Here's a version without all the conditions:

public boolean add(double amount) {
    if (balance + amount <= cap) {
        balance += cap;
        return true;
    } else {
        return false;
}

Please note the the LSP in its original version ("If for each object o_1 of type S there is an object o_2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o_1 is substituted for o_2, then S is a subtype of T") is violated. You have to define o_2 that works for each program. Choose a cap, let's say 1000. I'll write the following program:

Account a = ...
if (a.add(1001)) {
    // if a = o_2, you're here
} else {
    // else you might be here.
}

That's not a problem because, of course, everyone uses a weaken version of the LSP: we don't want the beahvior to be unchanged (subtype would have a limited interest, performance for instance, think of array list vs linked list)), we want to keep all "the desirable properties of that program" (see this question).

like image 26
jferard Avatar answered Oct 17 '22 10:10

jferard