9

In my quest to write better, cleaner code, I am learning about SOLID principles. In this, LSP is proving to be little difficult to grasp properly.

My doubt is what if I have some extra methods in my subtype, S, which were not there in type, T, will this always be violation of LSP? If yes, then how do I extend my classes?

For example, lets say we have a Bird type. And its subtypes are Eagle and Humming Bird. Now both the subtypes have some common behavior as the Bird. But Eagle also has good predatory behaviour (which is not present in general Bird type), that I want to use. Hence, now I won't be able to do this :

Bird bird = new Eagle();

So is giving Eagle those extra behaviour breaking LSP ?

If yes, then that means I can't extend my classes because that would cause LSP violation?

class Eagle extends Bird {
   //we are extending Bird since Eagle has some extra behavior also
}

Extending classes should be allowed in accordance with Open/Closed principle right?

Thank you in advance for answering ! As you can clearly see, LSP has got me confused like anything.

Edit: Refer this SO answer. In this again, when Car has additional behaviour like ChangeGear, it violates LSP. So, then how do we extend a class, without violating LSP?

user270386
  • 223
  • 1
  • 5
  • Possible duplicate of [How to verify the Liskov substitution principle in an inheritance hierarchy?](https://softwareengineering.stackexchange.com/questions/170189/how-to-verify-the-liskov-substitution-principle-in-an-inheritance-hierarchy) – gnat May 03 '18 at 08:20
  • I went through that, but it didn't answer my query. I have read lot of answers actually, but no help so far. – user270386 May 03 '18 at 08:40
  • 1
    it's right there in the top answer, have you read it: _Every time you derive one class from another, think about the base class and what people might assume about it... Then think "do those assumptions remain valid in my subclass?" If not, rethink your design._ – gnat May 03 '18 at 08:52
  • @gnat, Yeah I did, but I am little slow :) And I usually need more explanation than others might require. After reading David Arno's thorough answer, I am able to relate to that line now. – user270386 May 03 '18 at 10:36
  • @DavidArno unlike many down and close voters gnat tells us what his issue is. It's best to at least read what he points us at before dismissing it. – candied_orange May 03 '18 at 10:41
  • Rather than add method `attack()` on only `Eagle`, you'd instead have a method `feed()` on `Bird` which is implemented by both. You should generally make it work with inheritance, not against it. – Neil May 03 '18 at 10:47
  • @user270386: Regarding the `Car` violation mentioned in the SO answer you link to, Downcasting is a code smell, but it is in itself *not* a violation of LSP. It is more a mis-use of the inheritance hierarchy. For that reason, I don't agree that it is an example of a "glaring violation". – Bart van Ingen Schenau May 03 '18 at 10:47
  • @CandiedOrange, I tried that when I first achieved enough rep to vote to close too. I quickly worked out though that he "cries wolf" all the time. Very occasionally it is a duplicate, but then others pick that up too and then I'll take a look. I certainly do not waste my time checking every time he bleats though. What I do do, is reassure new folk that he isn't representative of the rest of us as he's a poor first encounter here. What I find particularly sad is that whenever I do that though, my comments get deleted. So there's at least one mod who implicitly endorses his negative behaviour. – David Arno May 03 '18 at 13:49
  • @DavidArno I know we're supposed to work on making the newbie experience more positive but when I was coming up gnat was a life line to understanding why I was getting spanked. None of us can represent all of us but gnat at least gets us talking about it. Sometimes I learn what the OP really wants when they explain to gnat why it's not a dupe. – candied_orange May 03 '18 at 14:31
  • Liskov type substitution is all about formal specifications for type behavior. These specifications apply to the base type and derived types; the derived type will add features as long as it does not violate the specification of the base type. Ideally the compiler checks these for you. – Frank Hileman May 03 '18 at 16:29
  • 1
    @FrankHileman many of us work with compilers and code bases that are less than ideal. Even if we didn't it's still a good thing when the humans also understand how to respect them. – candied_orange May 03 '18 at 18:10
  • @CandiedOrange Without specifications/contracts, with preconditions, postconditions, and invariants, type substitution has little meaning and is difficult to comprehend. Hence all the misleading blog posts about it. – Frank Hileman May 04 '18 at 18:41
  • @FrankHileman I always thought the best metaphor for types was power plugs and outlets. Sure the plug fits in the outlet but if all that's behind it is a 9 volt battery it's not gonna work, well unless you plug it into something that only needs 9 volts. – candied_orange May 04 '18 at 19:26
  • @FrankHileman I once worked out that each circuit of a string of programmable xmas lights was 45 watts. Had great fun rewiring them up to light bulb sockets and putting colored 45 watt bulbs in them. Worked perfectly. Gave it to a DJ friend of mine who loved it. Told him over and over not to put anything bigger than 45 watts in them. Guess how that went? – candied_orange May 04 '18 at 19:31

2 Answers2

10

My doubt is what if I have some extra methods in my subtype, S, which were not there in type, T, will this always be violation of LSP?

Very simple answer: no.

The point to the LSP is that S should be substitutable for T. So if T implements a delete function, S should implement it too and should perform a delete when called. However, S is free to add additional functionality over and above what T provides. Consumers of a T, when given an S would be unaware of this extra functionality, but it's allowed to exist for consumers of S directly to utilise.

A highly contrived of examples of how the principle can be violated might be:

class T
{
    bool delete(Item x)
    {
        if (item exists)
        {
            delete it
            return true;
        }
        return false;
    }
}

class S extends T
{
    bool delete(Item x)
    {
        if (item doesn't exist)
        {
            add it
            return false;
        }
        return true;
    }
}

Slightly more complex answer: no, as long as you don't start affecting the state or other expected behaviour of the base type.

For example, the following would be a violation:

class Point2D
{
    private readonly double _x;
    private readonly double _y;

    public virtual double X => _x;
    public virtual double Y => _y;

    public Point2D(double x, double y) => (_x, _y) = (x, y);
}

class MyPoint2D : Point2D
{
    private double _x;
    private double _y;

    public override double X => _x;
    public override double Y => _y;

    public MyPoint2D(double x, double y) : 
        base(x, y) => (_x, _y) = (x, y);

    public void Update(double x, double y) => (_x, _y) = (x, y);
}

The type, Point2D, is immutable; its state cannot be changed. With MyPoint2D, I've deliberately circumvented that behaviour to make it mutable. That breaks the history constraint of Point2D and so is a violation of the LSP.

David Arno
  • 38,972
  • 9
  • 88
  • 121
  • Perhaps a good addition to this answer would be an example of behaviour that could be added to such a `delete` function that _would_ be an LSP violation? – Andy Hunt May 03 '18 at 08:25
  • But if S implements extra functionality, won't it violate the history constraint? – user270386 May 03 '18 at 08:30
  • @AndyBursh, answer updated to show that. – David Arno May 03 '18 at 09:06
  • 1
    @user270386: No. LSP is not about the structure of the subclass, it's about the behavior of the subclass code, compared to the behavior of the base class. It's not the *extra functionality* by itself that violates the history constraint; rather, the constraint is violated if this new functionality does something that's unexpected (e.g. prohibited) in the base class (and this includes things that can be expressed through the language, *as well* as things that can only be expressed through documentation). – Filip Milovanović May 03 '18 at 09:07
  • @user270386, only if that extra behaviour does violate the history constraint. Extra behaviour doesn't automatically do so; it has to cause change to the behaviour of that the base class for the history rule to apply. I've updated my answer to address that. – David Arno May 03 '18 at 09:07
  • 1
    It may be worth noting that if T is a strict interface, fully abstract class, or even the null object pattern then S is pretty much about the structure. T is code. It's not necessarily a specification, a requirements document, or a product owner. We only care about LSP violations when T is being used to manifest a constraint that must always hold. Not every class does that. But If you tell S to delete and by design S does nothing that had better be OK with the rest of the code. T can't tell you if that's true. It can only tell you that you'd better check before you do this. – candied_orange May 03 '18 at 10:28
  • @CandiedOrange: OK, I may have not expressed myself with enough precision, and we may mean different things by structure, but, in any case, I'm not saying the structure is irrelevant. However, within the context of subtyping as defined by LSP, I'd say the structure on the subclass is enforced to guarantee that certain aspects of behavior prescribed by the base type are met (i.e., the subtype must respond to the same messages (must have same methods) as the supertype), but that is not enough, so LSP imposes stronger constraints. (continues) – Filip Milovanović May 03 '18 at 11:26
  • 1
    (continued) IMO, it's the requirement for substitutability that drives all this. Substitutability really stems from the ability to treat two different implementations (behaviors) as equivalent at some higher level of abstraction prescribed by the contract defined by the supertype. Structure is, in that sense, a means to an end. – Filip Milovanović May 03 '18 at 11:26
  • P.S. "But If [...] by design S does nothing that had better be OK with the rest of the code. T can't tell you if that's true. It can only tell you that you'd better check before you do this." - That's the whole point, though. If the language is not expressive enough so that T can tell you that in code, then you have to say it in the documentation. If T explicitly says that the delete operation should never do nothing (barring exceptions), then code that uses T (or a subtype) is justified in making that assumption, and an S that does nothing violates LSP, as it may not work with such code. – Filip Milovanović May 03 '18 at 11:34
  • @FilipMilovanović I can agree with that. But if that documentation lives in T it can suffer from the same problem. Some one creates T and demands that delete works because the system needs it to. Then the system changes and now it's ok for delete commands to be ignored. Said in code or in comments the delete constraint in T is now out of date. T simply doesn't know. All it can do is say "this used to be a thing so you'd better check before you mess with it" – candied_orange May 03 '18 at 12:09
  • Liskov type substitution refers to formal specifications, not to the presence or absence of methods, which is checked by the compiler in a statically typed language. Your answer would be good if you used specifications and instead of vague definitions. – Frank Hileman May 03 '18 at 16:31
  • @CandiedOrange This is why Liskov substitution doesn't mean much when your tools have use no machine readable specifications. Without such specifications checked by the compiler, or a language using dependent types, we are forced to use documentation and check manually. There is no Liskov substitution "principle"; it comes with the territory and you cannot compile without it if you have the right tools. – Frank Hileman May 03 '18 at 16:36
  • @FrankHileman ah I wondered when you’d turn up. The code of the base type is the primary specification as it describes its behaviour precisely. As others have said, supplementary specs can assist with this, but they remain only supplementary. – David Arno May 03 '18 at 16:37
  • 1
    @DavidArno "the code of the base type" -- is neither a specification, nor accessible necessarily. – Frank Hileman May 03 '18 at 16:38
  • This provides some examples of type specifications in various languages. Programming Languages, Third Edition, Chapter 11: https://mathcs.clarku.edu/~jmagee/csci170/slides/LL_ch11.pdf – Frank Hileman May 03 '18 at 16:50
  • This is an example of a language using dependent types, whereby the specification is part of the language, and checked by the compiler: https://www.fstar-lang.org/ – Frank Hileman May 03 '18 at 16:53
3

Of course not. If the Eagle object can be used by any code that expects a Bird or subclass, and behaves as a Bird should behave, you are fine.

Of course the Eagle behaviour can only be used by code that is aware that it is such an object. We would expect that some code will explicitly create an Eagle object and use it as an Eagle object, while being able to use any code that expects Bird objects.

gnasher729
  • 42,090
  • 4
  • 59
  • 119