29

I was following this highly voted question on possible violation of Liskov Substitution principle. I know what the Liskov Substitution principle is, but what is still not clear in my mind is what might go wrong if I as a developer do not think about the principle while writing object-oriented code.

Geek
  • 5,107
  • 8
  • 40
  • 58
  • 6
    What can go wrong if you don't follow LSP? Worst-case scenario: you end up summoning Code-thulhu! ;) – FrustratedWithFormsDesigner Oct 17 '12 at 16:00
  • 1
    As the author of that original question, I have to add that it was quite an academic question. Although violations can cause errors in code, I have never had a serious bug or maintenance issue that I can put down to a violation of LSP. – Paul T Davies Oct 18 '12 at 13:29
  • 2
    @Paul So yo never had a problem with your programs due to convoluted OO hierarchies (which you didn't design yourself, but maybe had to extend) where contracts were broken left and right by people who were uncertain about the purpose of the base class to begin with? I envy you! :) – Andres F. Oct 24 '12 at 15:24
  • @PaulTDavies the severity of the consequence depend on whether the users (programmers who use the library) have detailed knowledge of the library's implementation (i.e. have access to and familiar with the library's code.) Eventually users will put dozens of conditional checks or build wrappers around the library to account for non-LSP (class-specific behavior). The worst case scenario would happen if the library is a closed-source commercial product. – rwong Jul 19 '13 at 08:21
  • @Andres and rwong, please illustrate those problems with an answer. The accepted answer pretty much supports Paul Davies in that the consequences seem minor (an Exception) that will be quickly noticed and rectified if you have a good compiler, static analyzer, or a minimal unit test. – user949300 Jun 03 '15 at 21:15
  • @user949300 No, it doesn't. Also, minimal unit testing is not enough for non-toy situations. – Andres F. Jun 03 '15 at 23:15
  • @Andres, saying "No it doesn't" does not illustrate the problem with a real world example, which I requested. Please do so and I will gladly upvote it. – user949300 Jun 03 '15 at 23:25
  • @user949300 Well, your comment was unsupported as well. I wasn't trying to provide an answer; I'm quite satisfied with the accepted one. – Andres F. Jun 04 '15 at 00:08
  • @user949300 A matter of good OO design is mostly orthogonal to having "a good compiler or static analyzer" -- good OO design must work in most situations and for most OO languages. For example, if you do OOP with an *interpreted* language, how is the "compiler" going to help you then? And a "minimal test suite" is demonstrably not enough (give me your suite and I'll write code that breaks it); even a reasonable test suite won't catch many bugs. – Andres F. Jun 04 '15 at 20:36

9 Answers9

31

I think it's stated very well in that question which is one of the reasons that was voted so highly.

Now when calling Close() on a Task, there is a chance the call will fail if it is a ProjectTask with the started status, when it wouldn't if it was a base Task.

Imagine if you will:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

In this method, occasionally the .Close() call will blow up, so now based on the concrete implementation of a derived type you have to change the way this method behaves from how this method would be written if Task had no subtypes that could be handed to this method.

Due to liskov substitution violations, the code that uses your type will have to have explicit knowledge of the internal workings of derived types to treat them differently. This tightly couples code and just generally makes the implementation harder to use consistently.

Jimmy Hoffa
  • 16,039
  • 3
  • 69
  • 80
  • Does that mean that a child class can't have his own public methods that aren't declared in the parent class? – Songo Oct 17 '12 at 19:32
  • @Songo: Not necessarily: it can, but those methods are "unreachable" from a base pointer (or reference or variable or whatever the language you use calls it) and you need some run-time type information to query what type the object has before you can call those functions. But this is a matter that is strongly related to languages syntax and semantics. – Emilio Garavaglia Oct 17 '12 at 19:39
  • 2
    No. This is for when a child class is referenced as if it were a type of the parent class, in which case members which aren't declared in the parent class are inaccessible. – Chewy Gumball Oct 17 '12 at 19:40
  • So in other words, the more you violate the Liskov Substitution principle, the harder it is to maintain your code because you have to make changes in many places rather than just one place. – Phil Oct 18 '12 at 14:01
  • 1
    @Phil Yep; this is the definition of tight coupling: Changing one thing causes changes to other things. A loosely coupled class can have it's implementation changed without requiring you to change code outside of it. This is why contracts are good, they guide you in how not to require changes to your object's consumers: Meet the contract and the consumers will need no modification, thus loose coupling is achieved. When your consumers need to code to your implementation rather than your contract this is tight coupling, and required when violating LSP. – Jimmy Hoffa Oct 18 '12 at 14:52
  • While I understand and appreciate this answer, if this is the **worst** than can happen with a violation of LSP, then I'm with @Paul T Davies, the LSP is fairly "academic" and of relatively little importance in the real world. A good unit test (or in many cases **the compiler**) would catch that exception and lead to something like the `canClose()` option in the original question. I mean, Java Collection's somewhat infamous optional methods violate LSP in this manner and millions of people make it work for them. – user949300 Jun 03 '15 at 21:10
  • 1
    @user949300 This is not "fairly academic". If subclassing breaks existing methods which accept the superclass, this introduces a maintenance nightmare. You cannot introduce unit test to cover for every deviation a subclass can make, and you cannot predict them. Like this answer states, this problem can be seen as a case of tight coupling, which is a very serious and real-world software engineering concern! – Andres F. Jun 03 '15 at 23:19
  • @user949300 The Java Collections is a terrible counterexample, because they are known to have multiple historical problems. You can work around them, but it'd be better if they weren't there. Remember, at one time Java Collections didn't even support generics, and people still managed to write useful programs without them. Does this make generics useless, or "fairly academic"? :) – Andres F. Jun 03 '15 at 23:22
  • 1
    @user949300 The success of any piece of software to accomplish it's job is no measure of it's quality, long-term, or short-term costs. Design principles are attempts at bringing about guidelines to reduce long-term costs of software, not to make software "work". People can follow all the principles they want while still failing to implement a working solution, or follow none and implement a working solution. Though java collections may work for many people, that does not mean the cost to work with them in the long run is as cheap as it could be. – Jimmy Hoffa Jun 04 '15 at 00:38
  • Jimmy and Andres, please give a real world example, from your work, where not following LSP has hurt you. Sounds like you should have a good example from the well known horrible Java Collections. If it is a good example I will upvote. – user949300 Jun 04 '15 at 04:04
  • @user949300 I cannot give you an example of convoluted production code and don't have the time to distill it to its essentials. But Jimmy Hoffa's last comment hit the mark: it's not enough for software to "work", the costs of maintaining and refactoring it have to be taking into account. Otherwise, what's the point of having generics in Java, if collections of Objects work just as well? – Andres F. Jun 04 '15 at 20:32
  • it WOULD be reasonable to describe the `Close()` method as something that MIGHT fail depending on the state of the runtime type though right? it's one thing if the interface contract says "`Close()` always closes the task", but is it really so bad to say "`Close()` closes the task if it is permitted to do so at the moment"? – sara Apr 12 '16 at 12:30
13

If you don't fulfill the contract that has been defined in the base class, things can silently fail when you get results that are off.

LSP in wikipedia states

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.

Should any of these not hold, the caller might get a result he does not expect.

Zavior
  • 1,334
  • 2
  • 11
  • 18
  • 1
    Can you think of any concrete examples to demonstrate this? – Mark Booth Oct 19 '12 at 14:27
  • 1
    @MarkBooth The circle-ellipse / square-rectangle problem might be useful to demonstrate it; the wikipedia article is a good place to start: http://en.wikipedia.org/wiki/Circle-ellipse_problem – Ed Hastings Nov 16 '12 at 02:45
7

Consider a classic case from the annals of interview questions: you have derived Circle from Ellipse. Why? Because a circle IS-AN ellipse, of course!

Except... ellipse has two functions:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

Clearly, these must be re-defined for Circle, because a Circle has a uniform radius. You have two possibilities:

  1. After calling set_alpha_radius or set_beta_radius, both are set to the same amount.
  2. After calling set_alpha_radius or set_beta_radius, the object is no longer a Circle.

Most OO languages don't support the second, and for a good reason: it would be surprising to find that your Circle is no longer a Circle. So the first option is the best. But consider the following function:

some_function(Ellipse byref e)

Imagine that some_function calls e.set_alpha_radius. But because e was really a Circle, it surprisingly has its beta radius also set.

And herein lies the substitution principle: a subclass must be substitutable for a superclass. Otherwise surprising stuff happens.

Kaz Dragon
  • 878
  • 5
  • 12
  • 1
    I think you can run into trouble if you use mutable objects. A circle is also an ellipse. But if you replace an ellipse which is also a circle with another ellipse (which is what you are doing by using a setter method) there is no guarantee that the new ellipse will also be a circle (circles are a proper subset of ellipses). – Giorgio Oct 24 '12 at 15:23
  • 2
    In a purely functional world (with immutable objects), the method set_alpha_radius(d) would have return type ellipse (both in the ellipse and in the circle class). – Giorgio Oct 24 '12 at 15:25
  • @Giorgio Yes, I should have mentioned that this problem only occurs with mutable objects. – Kaz Dragon Oct 25 '12 at 08:48
  • @KazDragon: Why would anyone substitute an ellipse with a circle object when we know that an ellipse IS NOT A circle? If someone does that, they do not have a correct understanding of the entities they are trying to model. But by allowing this substitution, aren't we encouraging loose understanding of the underlying system that we are trying to model in our software, and thus creating bad software in effect? – darKnight Jul 16 '18 at 06:00
  • @maverick I believe you have read the relationship I described backwards. The proposed is-a relationship is the other way around: a circle is an ellipse. Specifically, a circle is an ellipse where the alpha and beta radii are identical. And so, the expectation might be that any function expecting an ellipse as a parameter could equally take a circle. Consider calculate_area(Ellipse). Passing a circle to that would yield the same result. But the problem is that the behaviour of the mutation functions of Ellipse are not substitutable for those in Circle. – Kaz Dragon Jul 17 '18 at 14:45
  • @KazDragon I see your point and understand its theoretical argument, but can you list a real world example where not following LSP would actually be surprising and more importantly, cause unexpected behaviour or a bug? Even in above example, the result of passing circle instead of ellipse to some_function should not surprise the user as he is trying to cause a fundamentally incorrect behavior on circle by only setting its alpha radius. I would argue that this is a flaw in implementation by the user rather than an unexpected behaviour by the API (as it's maintaining the integrity of the circle) – darKnight Jul 17 '18 at 16:39
  • @maverick This is really difficult to do, because any example would give the result, "well that's obviously wrong!" and that's kind of the point. LSP is _why_ those things are wrong. Consider a filesystem that hosts any file type that has associated open/close/read/write operations. A particular file type implementation requires a new function init to be called after open, before reading or writing, otherwise it fails to read or write. This fails LSP because any code written for the basic file types will not work with the new function: the new file type is not substitutable. – Kaz Dragon Jul 19 '18 at 07:22
6

In layman's words:

Your code will have an awful lot of CASE/switch clauses all over.

Every one of those CASE/switch clauses will need new cases added from time to time, meaning the code base is not as scalable and maintainable as it should be.

LSP allows code to work more like hardware:

You don't have to modify your iPod because you bought a new pair of external speakers, since both the old and the new external speakers respect the same interface, they are interchangeable without the iPod losing desired functionality .

Tulains Córdova
  • 39,201
  • 12
  • 97
  • 154
  • 2
    -1: all around bad answer – Thomas Eding Oct 17 '12 at 20:12
  • 3
    @Thomas I disagree. It's a good analogy. He talks about not breaking expectations, which is what the LSP is about. (though the part about case/switch is a bit weak, I agree) – Andres F. Oct 24 '12 at 15:26
  • 2
    And then Apple broke LSP by changing connectors. This answer lives on. – Magus Feb 26 '14 at 23:17
  • I don't get what switch statements have to do with LSP. if you're referring to switching over `typeof(someObject)` to decide what you're "allowed to do", then sure, but that's another anti-pattern entirely. – sara Apr 12 '16 at 12:33
  • A drastic reduction in the amount of switch statements is a desirable side effect of LSP. As objects can stand for any other object that extends the same interface, no need for special cases to be taken care of. – Tulains Córdova Apr 12 '16 at 12:38
  • +1 because I like Apple. – NingW Apr 17 '19 at 03:02
1

to give a real life example with java's UndoManager

it inherits from AbstractUndoableEdit whose contract specifies that it has 2 states (undone and redone) and can go between them with single calls to undo() and redo()

however UndoManager has more states and acts like an undo buffer (each call to undo undoes some but not all edits, weakening the postcondition)

this leads to the hypothetical situation where you add a UndoManager to a CompoundEdit before calling end() then calling undo on that CompoundEdit will lead it to call undo() on each edit once leaving your edits partially undone

I rolled my own UndoManager to avoid that (I probably should rename it to UndoBuffer though)

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

Example: You are working with a UI framework, and you create your own custom UI-control by subclassing the Control base class. The Control base class defines an method getSubControls() which should return a collection of nested controls (if any). But you override the method to actually return a list of birthdates of presidents of the United States.

So what can go wrong with this? It is obvious that the rendering of the control will fail, since you don't return a list of controls as expected. Most likely the UI will crash. You are breaking the contract which subclasses of Control is expected to adhere to.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
0

You can also look at it from a modelling point of view. When you say that an instance of class A is also an instance of class B you imply that "the observable behavior of an instance of class A can also be classified as observable behavior of an instance of class B" (This is only possible if class B is less specific than class A.)

So, violating the LSP means that there is some contradiction in your design: you are defining some categories for your objects and then you are not respecting them in your implementation, something must be wrong.

Like making a box with a tag: "This box contains only blue balls", and then throwing a red ball into it. What is the use of such a tag if it shows the wrong information?

Giorgio
  • 19,486
  • 16
  • 84
  • 135
0

I inherited a codebase recently that has some major Liskov violators in it. In important classes. This has caused me huge amounts of pain. Let me explain why.

I have Class A, which derives from Class B. Class A and Class B share a bunch of properties that Class A overrides with its own implementation. Setting or getting a Class A property has a different effect to setting or getting the exact same property from Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

Putting aside the fact that this is an utterly terrible way to do translation in .NET, there are a number of other issues with this code.

In this case Name is used as an index and a flow control variable in a number of places. The above classes are littered throughout the codebase in both their raw and derived form. Violating the Liskov substitution principle in this case means that I need to know the context of every single call to each of the functions that take the base class.

The code uses objects of both Class A and Class B, so I cannot simply make Class A abstract to force people to use Class B.

There are some very useful utility functions that operate on Class A and other very useful utility functions that operate on Class B. Ideally I would like to be able to use any utility function that can operate on Class A on Class B. Many of the functions that take a Class B could easily take a Class A if it was not for the violation of the LSP.

The worst thing about this is that this particular case is really hard to refactor as the entire application hinges on these two classes, operates on both classes all the time and would break in a hundred ways if I change this (which I am going to do anyway).

What I will have to do to fix this is create a NameTranslated property, which will be the Class B version of the Name property and very, very carefully change every reference to the derived Name property to use my new NameTranslated property. However, getting even one of these references wrong the entire application could blow up.

Given that the codebase does not have unit tests around it, this is pretty close to being the most dangerous scenario that a developer can face. If I don't change the violation I have to spend huge amounts of mental energy keeping track of what type of object is being operated on in each method and if I do fix the violation I could make the whole product explode at an inopportune time.

Stephen
  • 8,800
  • 3
  • 30
  • 43
  • What would happen if within the derived class you shadowed the inherited property with a different kind of thing that had the same name [e.g. a nested class] and created new identifiers `BaseName` and `TranslatedName` to access both the class-A style `Name` and the class-B meaning? Then any attempt to access `Name` on a variable of type `B` would be rejected with a compiler error, so you could ensure that all references got converted to one of the other forms. – supercat Dec 23 '13 at 21:19
  • I no longer work at that place. It would have been very awkward to fix. :-) – Stephen Jan 07 '14 at 01:01
-4

If you want to feel the problem of violating LSP, think what happens if you have only .dll/.jar of base class (no source code) and you have to build new derived class. You can never complete this task.

sgud
  • 101
  • 1