21

Overriding a method originally defined in the super class, by definition means this method will do different things when invoked on an object of the base class or an object of the subclass.

So does this mean that overriding methods always means violating LSP? Or are there cases when it doesn't?

gnat
  • 21,442
  • 29
  • 112
  • 288
Aviv Cohn
  • 21,190
  • 31
  • 118
  • 178

5 Answers5

34

LSP forbids to violate the contracts of a supertype in a subtype, it does not forbid to change the behaviour of any method (within the bounds of that contract).

For example, lets assume you have a superclass Report, associated with a certain object Foo, with a method ToString(), and subtypes HTMLReport, XMLReport and TextReport. Lets further assume Report is not abstract and the default implementation of ToString() is to return the empty string. Now you define a contract in the following manner:

  • ToString() shall deliver a string with a textual representation of Foo in a certain text format (and the empty string if the format is not defined so far).
  • ToString() shall not mutate the Report object
  • ToString() shall never throw an Exception

So the subclasses can easily override the ToString method, each one implementing a different behaviour, but all perfectly following the LSP.

On the other hand, if your contract would be

  • ToString() shall always return the empty string

then overriding it and return something different would violate the LSP - but such a contract would obviously make not much sense for any real world scenario.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • 5
    In a professional environment (I'm a hobbyist), would this 'contract' be defined anywhere? E.g. in the documentation? Or is it only in the programmer's head? – Aviv Cohn Jun 12 '14 at 13:15
  • 3
    @Prog: I would not see this as a property of "professional environments". As you might have noticed, "Design by contract" is explicitly mentioned at the top of the Wikipedia article about LSP (http://en.wikipedia.org/wiki/Liskov_substitution_principle). The support in different programming languages is different, some languages allow explictly the definition of post-/preconditions and invariants. The contract can always be defined in the documentation (including examples or unit tests, which are part of the docs). – Doc Brown Jun 12 '14 at 13:33
  • Non-private members (function signatures, properties, fields) are also part of the contract. Generally, statically typed languages enforce this during compilation. – Brian Jun 12 '14 at 19:00
  • @Brian: absolutely, sometimes I forget the obvious things. – Doc Brown Jun 12 '14 at 20:13
  • I am glad you emphasized the pre/post-conditions and invariants. Too often an interface is used as an example, with no specification to create a real abstraction. – Frank Hileman Jun 12 '14 at 23:21
  • I've been thinking about this. If this is what LSP says, then I don't feel it is strong enough. If a subclass changes **any observable behaviour** of a super class then it's no longer a subtype as it can't be substituted for the super type in all situations. There are still some situations where you can override and preserve behaviour. Eg – you might override a message handler and call super and _also_ handle additional messages. Then you (somewhat arguably) _are_ a true subtype of the superclass. It seems like a very easy situation to slip up with though. – Benjohn Aug 19 '16 at 21:34
  • You can have an abstract interface / protocol, and subtype that or conform to it (depending on your terminology). Then you _are_ a true subtype, because the interface has explicitly deferred implementation and you are filling that in. The subtype is a strict superset of the super type, which it must be. – Benjohn Aug 19 '16 at 21:41
  • @Benjohn: If a subtype would not change *any* behaviour, it would not even be a subtype, it would be the equal to the supertype (maybe only with another name). But when you say a subtype *can* change behaviour as long as it conforms to an "abstract interface / protocol" - that is exactly what is called a contract. – Doc Brown Aug 19 '16 at 22:24
  • @DocBrown Right – I agree … it can change behaviour that is explicitly not defined – is abstract in some way. … when you talk about "contract", is that what I would be thinking of as akin to a "protocol" in swift, say – essentially a type based definition of the "contract"? Or is it more that that? Perhaps with preconditions and post conditions? As a silly example, a Limerick is a Poem. A Limerick has a particular pentameter. Assuming our type system is not able to describe pentameter, we'd still like it to be in the contract that Limericks and sub types have the correct pentameter. – Benjohn Aug 21 '16 at 11:20
  • 1
    @Benjohn: I added a link to the word "contract" in my answer, should clarify. I already mentioned what I meant by this term in the second comment below my answer. – Doc Brown Aug 21 '16 at 11:43
2

First simple case: adding caching to an expensive operation. there is no functional difference between the original and the new function.

Also the original function could be documented to do something and as long as the overriding function still fits within that documentation then there is no problem (the core of the substitution principle). Remember you should code against the API not the implementation.

ratchet freak
  • 25,706
  • 2
  • 62
  • 97
1

The Liskov Substitution Principle only says that subclasses should not violate provable properties of the supertype. The provable properties are basically the type signatures of members. So if a method on a superclass is declared to return an integer, then it shouldn't be overwritten in subclass to return a string.

Type systems in modern statically-typed OO languages generally prevent this from happening, so as an ordinary developer of say C# code you shouldn't be too worried about breaking the LSP (although I believe there are some weaknesses in the type system regarding Arrays which allows you to break LSP).

The principle is often understood more broadly (and vaguely) that subclasses should not "break expectations", which definitely a good design principle, but not really what the LSP says.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
  • I'm confused by the assertion that LSV is mostly type-checkable. How does that square with e.g. the history rule, or enforcement of contracts. It seems clear from this that LSV is about behavior: https://en.wikipedia.org/wiki/Behavioral_subtyping (and if even if it wasn't, prior work by Meyer directly deals with contracts & behavior). Do you have a source? – johncip Feb 16 '22 at 19:56
0

By definition different things? Whaddayoumean?

Liskov says you should not "go against" your ancestor's behavior. Doesn't mean that you cannot extend or augment it by doing stuff in addition to that base behavior.

In your overridden method you call inherited to execute the ancestor's behavior (honoring Liskov) and then can do stuff in addition to that.

Not calling inherited at all (*) would be a Liskov violation (unless you re-code the base's implementation, which I'd strongly advice against). Update @Ratchetfreak actually has a pretty neat example where a descendant would only call inherited if it hadn't already cached a result for an otherwise "expensive" calculation.

(*) please note that I am used to a language where you can call inherited regardless of whether the ancestor or the ancestor method is marked abstract. The compiler takes out the call.

Marjan Venema
  • 8,151
  • 3
  • 32
  • 35
  • 2
    More generally, LSP is predicated on sub*type* behaviour, not sub*class* behaviour. Take a List abstract datatype, for example. "List" has no behaviour inherently, it has a contract it must hold to. An array is a list, as is a proxy to a remote ListServer. They operate in entirely different ways, but both hold to the contract. There's no "base behaviour" there, they just need to be swappable. – Phoshi Jun 12 '14 at 11:09
  • suggesting that you should almost always call the base method when overriding strikes me as wrong as it implies your method now breaks the SRP – jk. Jun 12 '14 at 12:36
  • @jk. How? Augmented behavior in payment programs could be a getting a value from somewhere for the calculation of rebates, an additional rebate, ... whatever ... all variations on calculating (base) rebates from the ancestor, all still within SRP. – Marjan Venema Jun 12 '14 at 17:57
  • @Phoshi: Yes, so? That doesn't preclude cases where you do have base behavior and descendants can add to it as necessary. After all a class is a type. Calculations in payment and salary programs for example. The ancestor will do the basic calculations, descendants may add extra's, effect different values for parts of the calculation depending on whatever. Of course in any salary program the calculations may well be an amalgamation of Strategy patterns and each may in effect have its own inheritance hierarchy to deal with all variables and variations. – Marjan Venema Jun 12 '14 at 18:03
  • @MarjanVenema: Because you're focussing on specific examples instead of getting to the core reasoning. You give specific examples of places where to not break LSP you use inheritance, but that's all they are--specific examples, not at all the general case. – Phoshi Jun 15 '14 at 15:48
  • @Phoshi: Ah that's what you were getting at. Yes, indeed, I just responded to the "is overriding always a violation of LSP". And for me overriding is linked to classes... I trust you upvoted DocBrown's answer then :) – Marjan Venema Jun 15 '14 at 19:35
0

As an example, if you have a class with instances representing a user interface element, and that class has a "draw" method, then the "draw" method specification will not say exactly what will be drawn. It will instead say "the user interface element will be drawn appropriately to inform the user about all the state of the user interface element that is important to the user, in an intuitive and nice looking way". If you subclass that user interface element, the subclass draw method should and likely will act exactly according to the spec.

As a general principle, if the Liskov Substitution Principle made it impossible to subclass, that would be pretty a pretty stupid, and pretty stupid principles don't get names and Wikipedia entries and are quickly forgotten :-)

(BTW. It is an art that is too often forgotten to write a proper spec for a method that is supposed to be overridden in a subclass, because any existing behaviours do not describe the spec, only a special case of it).

gnasher729
  • 42,090
  • 4
  • 59
  • 119