The Liskov Substitution Principle Demystified

I apologise for the pompous title; I wrote it just for fun. If this “demystifies” anything, I’ll consider that a coincidence.


This is a companion discussion topic for the original entry at http://blog.thecodewhisperer.com/permalink/liskov-substitution-principle-demystified

LSP really is the stand-out in the group and not just because of the name. While the other priciples are concerned with coupling and cohesion, LSP stands alone in dealing with code correctness. Personally, I've always preferred the design-by-contract formulation since this applies to all code you write, not just types and subtypes (which are out of fashion in some circles).

I would say that superclasses clients are constrained to their restricted domain, and subclasses can extend this domain. All they have to do is preserving the "old behavior" for the old clients in regular (for those old clients) conditions, so that excludes what happens when contract is violated, because contract must not be violated.

For example the old client use superclass Money that prescribe "only positive values" (thus using negatives are not allowed and raise exceptions, but should not be used). The subclass is "negative money, i.e. debt are allowed", so the precondition is weakened (as design by contract suggests), witch is handled by "no exceptions are raised anymore for negatives.

Now the tests meant to check "irregular behavior" for the superclass (precondition violation) will behave differently for subclasses, because this negativity is not a violation anymore in the Subclasses.

So to summarize: contract for superclass: no negatives.
Test that verify the "behavior" in irregular use: create an exception.
contract for subclass: negatives allowed
Test that did verify the "behavior" in irregular use for superclass, should change because there is no exception anymore for using negatives

A consequence seems to be if some client include as "contract" the idea of expecting an exception for irregular use, this client cannot use a subclass in substitutions for the class. I don't know who is wrong in this case (if the client is right, then the "subclassing" we made is wrong).

A conclusion could be that simply respecting the contract is not: manage the exception that is eventually raised for contract violation, but, moreover: know the contract and just don't violate it.

In that case "old clients" (guys that carefully manage only positive money) will continue working good, even with new implementations/subclasses that, incidentally, now accept other "new clients" that knows that some negative amount are now allowed, with a behavior "somehow" compatible with the old one.

However there is some code. http://pastie.org/5650162

Your example clearly breaks LSP. The property is "the value of a Money in cents is never negative". That's provable about Money, but broken by MoneyWithNegativeStates.

Changing Money from "always >= 0" to "any amount" sounds like a very bad idea to do with inheritance. This is no longer "a different way of implementing the same behavior", but instead new behavior. Compare this to reimplementing Map or List to provide different performance characteristics, or reimplementing CustomerRepository to switch from MySQL to Gemstone. This is the essence of LSP: knowing when you are changing fundamental behavior (breaking LSP) compared to reimplementing existing behavior in a beneficial way (using a data structure optimised for reading when you discover that 97% of your operations are reading).

In this situation, I would introduce a class MoneyAllowingNegativeValues, which delegates to Money. I would then change all the clients of Money to use MoneyAllowingNegativeValues. Now that nobody uses Money any more, I would move its behavior into MoneyAllowingNegativeValues until Money disappears. At this point, I can now safely rename MoneyAllowingNegativeValues to Money. (Of course, if you prefer, you can rename Money to MoneyRequiringPositiveValues at the first step.)

Another approach involves changing the contract of Money in place, which I find a bit more dangerous, but in a smaller system, I would do it. First, I change the contract tests for Money which used to reject negative values, but now allow them. Then I would look for code that assumes that Money will reject negative values, and I change it. I don't like this approach, because the system is broken until the entire change is completed.

This comes to a fundamental misunderstanding about "extending behavior". Allowing negative values does not "extend" the behavior of Money; it *changes* a *fundamental assumption* that clients make about Money. To *extend* the behavior of Money, you might add the ability to understand currency, or you might add the ability to do arithmetic with Money, but you preserve all existing behavior of Money. The point of LSP is this: don't use inheritance to change a fundamental assumption that clients make about the behavior of the supertype.

I don't think they're "out of fashion", but rather more loosely defined. As soon as you have clients depending on you, you have a contract and you define a type. Your language doesn't have to describe that type with an interface or protocol, but the type nevertheless exists.

I agree, but I wanted only to make a (extremely dangerous and controversial, I admit) example that nonetheless is still compliant with the design by contract inheritance rule that says "precondition can be weaker". If we assume that clients do not violate the contract, and they shouldn't, nothing changes even using new subclasses, as a substitutes, with weaker precondition built around design by contract rules. But we may just argue that being compliant to design by contract is not a sufficient condition to respect the lsp principle - for example if we legittimately consider the behavior related to the use of values that break the contrat, as negtives in this case. After all I would say that the clients code usually is not build around assumptions of "never violate the contract", in term of this constraint (from wikipedia http://en.wikipedia.org/wik... "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." Many thanks for the post and the answer again.

From Object Oriented Software Construction (2nd Edition) page 340: A precondition expresses the constraints under which a routine will function properly...A precondition applies to all calls of the routine, both from within the class and from clients. A correct system will never execute a call in a state that does not satisfy the precondition of the called routine.

So by violating a precondition, the code that is calling the Money constructor with a negative amount causes your whole program to be incorrect.

I disagree that the precondition is weaker; I think it has changed in an incompatible way. If Money had made no promise about the behavior when the value is negative, then I would agree, because then clients would not have expected an exception when Money was negative and therefore they might not have to change once we clarify the behavior when Money has negative values.

But when Money said, "value < 0 means exception", it created a precondition related to the domain of values < 0, and when you introduced a subclass, you changed an existing precondition.

I have never read a formal description of "weakening a precondition", so it's possible that I don't use the term the way Meyer or others intended it.

Thanks, Philip. This seems to support my interpretation: if Money *didn't define* behavior when value < 0, then MoneyWithNegativeState would have no contract to violate.

Of course, now that I look at it further, MoneyWithNegativeState, as Tony wrote it, doesn't violate the contract of Money, because Money's contract only says "constructor rejects value < 0" and does nothing to enforce the invariant that value can never be set to 0 by some other method. That makes Money's implementation of the contract ambiguous and incorrect.

By now we're getting into some arcane territory. I don't expect everyone to go there with me. :)

The point that I did not clarify is that to make the example "dbc-frameworkless", I coded the logic that raises the exception as a substitute notation for precondition checking. If I used a dbc framework I could be writing something like this (before the construtor):

@pre(value>=0) // will raise in run time something like "contract violation exception" when value < 0

Perhaps now seems more reasonable that accepting negatives in subclasses is actully precondition weakening

So from
@pre(value>=0) )
a weaker precondition may be weakened like this
@pre(true)
or simply
@pre

For old clients _that respect the contract_, the "behavior" is still the same using subclasses with weaker precondition (but those old client don't know, and don't have to).

Just to clarify: I did some experiment some times ago about such cases, trying to figure out strange behavior, including the "equals" code (in term of using instanceOf vs getClass() or other implementations for example). Me too, I am still not sure if that kind of stuff was what Meyer meant.

I think I understand. We still have the difference between the precondition violation (constructor rejects value < 0) which you wrote and the invariant violation (value field never < 0), which you never wrote.

You couldn't weaken the precondition without weakening the invariant (this wouldn't achieve anything), and changing the *invariant* creates an incompatible contract.

I *think* I have that right. :)

Agree if the invariant that I may want to write is like "@ensures(valueField >=0)" .

However, I don't think I'll write this invariant.
I may want to write rather an invariant like this: @ensures (value==valueField) (http://en.wikipedia.org/wik...,

That will still ensure that the valueField will be "sound" anytime in any future implementation/subclass , even when constructor preconditions are weaker.

Indeed. I'd rather make Money immutable and avoid the problem altogether. Another vote in favor of immutable Value Objects.

I tried to express better my point in a separated document. A draft is already available (see one of my last tweets, or ask me if you'd like).