10 ms·
The Liskov Substitution Principle (2019)
- sumanthvepa 6y agoBTW if anyone is wondering why deriving class Square from class Rectangle is an error, it is primarily because the interface of the Rectangle class is mutable. If instead the Rectangle class was not mutable then deriving a square from it would be perfectly fine. Discussion on StackOverflow: https://stackoverflow.com/questions/1030521/is-deriving-square-from-rectangle-a-violation-of-liskovs-substitution-principle https://stackoverflow.com/questions/1030521/is-deriving-squa...
- mjburgess 6y agoI disagree with the mutability argument... the Square constructor is "not a subset" of the Rectangle constructor, Compare: new Square(side) new Rectangle(width, height) In this way, a Rectangle is more like a child of a square: widening its capabilities.
- BeetleB 6y ago> In this way, a Rectangle is more like a child of a square: widening its capabilities. Yes, but this will violate the Liskov Substitution Principle, which (effectively) states that any program that is correct for a class (Square) should be correct for the subclass (Rectangle). So a program that takes in a parent class (Square) can rely on the sides being equal. Now pass in a child class (Rectangle) and your program may break. Hence the violation.
- brlewis 6y agoMaking Rectangle a child of Square would violate the principle, because code that works correctly for squares might not work correctly for rectangles in general.
- andrewla 6y agoConstruction of an object is a function of the class, not a function of the object.
- chrisoverzero 6y agoSubclasses get narrower (that is, more specific) than their parents. Consider those constructors. In order to constructor-chain from a Square up to a Rectangle, duplicate the single parameter: Square(side) : Rectangle(side, side) There's not a well-behaved way to go in the other direction. Rectangle(width, height) : Square(width) // ??? Rectangle(width, height) : Square(height) // ???
- pdpi 6y ago> Subclasses get narrower (that is, more specific) than their parents. That's precisely the problem. Subclasses have to less restrictive than their parents. If a subclass adds observable restrictions, it fails the LSP.
- Twisol 6y agoConversely, if a subclass adds observable flexibility -- such as the ability to stretch along a single axis -- it also fails the LSP. Subclasses can really only add orthogonal properties, such that influencing one of these properties doesn't perturb properties observable from the super-interface.
- BeetleB 6y ago> Conversely, if a subclass adds observable flexibility -- such as the ability to stretch along a single axis -- it also fails the LSP. That's not a given. If the new capability is only in new methods of the subclass, then the program expecting the parent class will never be impacted by it. To be fair, I suppose you agree: > such that influencing one of these properties doesn't perturb properties observable from the super-interface.
- Twisol 6y agoRight, that's why I qualified both times with "observable". ~_^ It's worth noting that even if this capability can only occur due to methods on the subclass alone, it can still fail LSP if those manipulations are still observable to a client using only the superclass. This is pretty much only possible because multiple clients can have a reference to the same object, since it allows one client to observably transition the object into a state that should be unreachable from the perspective of the second client. (This is the root of the need to make "defensive copies" [1].) If one imposes a strict single-ownership discipline, then it doesn't matter if super-interface properties can be violated by subclass-only methods, because for the duration of its use via its super-interface, those violations are impossible (and hence unobservable). [1] http://www.javapractices.com/topic/TopicAction.do?Id=15 http://www.javapractices.com/topic/TopicAction.do?Id=15
- J-Kuhn 6y agoConstructors are not inherited. If you know you are constructing a Rectangle, then you use it's constructor. If you create a Square, you use that constructor. But they are not inherited, and can have completely different semantics - accept different arguments or throw for for new reasons. That having said, the Rectangle / Square example doesn't really work: Is `new Rectangle(42, 42)` a Square? (This brings us to factory methods...)
- crdrost 6y agoYou are getting close! It is both. So here the LSP violation being described is that a function which handles a Rectangle it is given properly (i.e. it does not construct it) might fail when given a subclass of Rectangle, precisely due to OOP fundamentals about late binding and letting the runtime class drive the runtime behavior. But your hunch that subtypes widen "capabilities" is also formally correct: if P is a subtype of Q then we expect that the function type P → X for arbitrary X is a supertype of Q → X. So if you have like a list of squares and you want to map over it, map requests a Square → X function but we understand intuitively that you should always be able to provide a Rectangle → X function in this argument slot, too. This is not just abstract... I forget where in the docs TypeScript starts talking about certain things being incorrect because other things are not contravariant/covariant/invariant, but yeah: TypeScript is trying to implement a practical subtyping system and technically subclasses are not subtypes of their parent classes in TypeScript’s original vision, so pragmatism forces you to say “just pretend they are!” and technically the subtype system is unsound. The problem is that in TypeScript’s theory a type is just the full interface of object properties on a value, including all their methods. Methods always look like functions with an implicit "this" argument type. So this becomes a problem precisely because the type of Rectangle looks like type Rectangle = { height: number, width: number, someMethod: (this: Rectangle) => something } And that recursive definition in terms of having those methods makes a type technically terminal (no subtypes can exist). So it is precisely that we can overload methods with implementations more specific to the subtype, which stops the subclass from being a subtype of the superclass.
- pacala 6y agoThe type of the constructor is not the type of the object. The type of Rectangle constructor is (number, number) => Rectangle, vs the type of Rectangle which is just Rectangle. Think RectangleFactory vs. Rectangle.
- nayuki 6y agoIndeed. When you read from a Rectangle, you want it to behave like a Rectangle or a more specific subclass. When you write to a Rectangle, you want it to behave like a Rectangle or a more general superclass. This is related to the PECS principle in generics: class List<E> { appendToMyself(List<? extends E> producer); copyMyselfTo(List<? super E> consumer); }
- ttymck 6y agoAre you referring to "contravariance/covariance"? If so , is that the same as if I suggested that calling the Rectangle method `setWidth` on a Square should return a Rectangle (that is _possibly_ a Square)? i.e. val mySquare: Square = new Square(20) val myMutatedSquare: Rectangle = mySquare.setWidth(40) // 20 x 40
- pif 6y agoAlternatively, you may distinguish between (a) objects representing one rectangle and (b) objects representing one rectangle at a time. Object of type (a) have only getters in their public API. Setters imply type (b). And, while each square is a rectangle, the set of all possible square is smaller than the set of all possible rectangles.
- vlasev 6y agoMathematically the set of squares and the set of rectangles have the same cardinality as there is a bijection between them. To see this, a square can be described by a single number, putting the set of squares in 1 to 1 correspondence with the real number line. A rectangle can be described by two numbers, so the set of rectangles is in 1 to 1 correspondence with the plane. The line and the plane have the same cardinality, so this means the sets have the same size. For example: https://math.stackexchange.com/questions/243590/bijection-from-mathbb-r-to-mathbb-rn https://math.stackexchange.com/questions/243590/bijection-fr...
- Twisol 6y agoYou aren't technically wrong, but in the colloquial sense of the comment you responded to, "smaller than" means "has a natural non-surjective inclusion into".
- kmill 6y agoYeah, and putting a natural measure on the space of rectangles, squares are a measure-zero subset. In other words there are so many rectangles, almost no rectangles are squares.
- Twisol 6y agoOoh, I like that one. For observers: fix the lower-left corner of your rectangles at the origin. Then a rectangle is determined by the position of its upper-right corner within the upper-right quadrant. The set of all rectangles is given by the entire quadrant. The set of all squares is the 45-degree line from the origin cutting the quadrant in half. The natural measure on a set of points in this space is "area". The entire quadrant has infinite area, but no matter how infinite in extent the line is, it has zero area. (In the context of probability, of which measure theory is an application, the likelihood of throwing a dart at the upper-right quadrant and hitting the line of squares is nil.) (You might argue that my choice of origin is "not natural", but it gives the same scheme as the natural "rectangle is (width, length) pair" and is a little more graphically intuitive. Every choice of corner as origin would give an isomorphic setup, resulting in the same subset-zero argument.)
- kerkeslager 6y agoYeah, this is a great example of how mutability can introduce a lot of subtle complexities.
- memco 6y agoI think the concept of the article is great but the explanation could use some work. Given the code it does not seem clear to me why the test case of an explicitly requested rectangle now returns functions from a square that was not initialised or requested in the setup. Maybe this is specific to the language the author is using?
- juanorozcov 6y agoThanks! I will try to make the explanations clearer, I'm still trying to improve my writing and this is very much appreciated, oh, and the code is Ruby. I forgot to add the source code for the article, I just pushed it to a git repo and added a link to it at the article's footer. Thanks again!
- TheTaytay 6y agoThanks for the article! In school, I believe I heard this summarized as something along the lines of: "a derived class should require no more and deliver no less than its base class". I have always liked that succinct phrasing of the principle.
- juanorozcov 6y ago"a derived class should require no more and deliver no less than its base class" Uh, very nice, I need to write that down, it's by far the most concise version of the principle I've heard! Thank you
- antman 6y agoI really thought the article ended somewhere in a middle. Really would like to see the 'end' code!
- vaer-k 6y agoI think this article is incomplete without showing an example of a solution implementation that satisfies the LSP
- loopz 6y agoIf you absolutely must use subclassing, remember that whatever code is operating on your base class, would need to be able to operate all your subclasses in identical fashion without introducing accidental surprises and unanticipated side-effects. If this seems restrictive, please consider that changes to one unit of code should ideally be unrelated to other separate units of code, and how to better accomplish this. Later usage of subclassing can at any time introduce similar leakages of encapsulation and temptations around that theme.
- pdpi 6y agoOne apparently minor change in Kotlin coming from Java is that classes are final by default, and can be made inheritable from by qualifying them with `open`, instead of being open by default and requiring This seems small, but is a major shift in mentality: Classes shouldn't be inherited from unless they were designed to be inheritable from, and this usually comes with non-trivial invariants you need to document and respect (one aspect of this being the LSP). Therefore, inheritability should be opt-in rather than opt-out.
- csharptwdec19 6y agoC# has had a similar mindset but different approach; Classes are not 'final'(in C#, 'sealed') by default, nor are methods, BUT they are not considered virtual by default. This still makes inheritance more 'opt-in' than 'opt-out', but still facilitates the use of 'container' classes. It's still open for expansion but you can't -really- change the implementation.
- gHosts 6y agoIn fact, skip all the natter about subtypes and the like and think _purely_ in terms of class invariants. A class invariant holds at the end of the constructor and the start of the destructor and at the start and end of every public method. A class invariant check for the child class _must_ invoke the class invariant check of the parent class up the class hierarchy. And that is the point and the whole point of the exercise. You can utterly rely every on instance of the parent class or any descendent class obeying the class invariant for the parent class. This allows you to reason about a broader group of types without getting bogged down by concrete details.
- Noumenon72 6y agoWhat's the invariant for the problem in the article, where set_height on a square mutates @width? Seems like you don't want to prevent width from mutating everywhere, you just want to ensure it doesn't mutate during set_height.
- pizlonator 6y agoI heard a rumor that Liskov learned of this principle from one of her students and then presented it as her own.
- onemoresoop 6y agoInteresting. Do you have any reference?
- pizlonator 6y agoYes, don’t feel like sharing. You’d have to buy me a beer in person.
- Jtsummers 6y agoThen don't share a rumor that seems to be intended to dismiss or diminish someone.
- pizlonator 6y agoThat leads to lack of accountability. So, no.
- Jtsummers 6y agoIf your objective was to increase accountability, congratulations, you've failed.
- skybrian 6y agoCan't find a reference, but I thought it was named after her by someone else?
- pizlonator 6y agoIt’s possible. Part of the story is that someone gave her the idea and then she gave a talk about it, not citing that person, and then everyone just assumed it was her idea. Lesson to folks in a leadership position with smart folks reporting to them: let them present their own ideas and get the credit. If you present their idea for them, make abundantly clear it’s their idea not yours.
- gregkerzhner 6y agoI feel like the real Liskov Subsitution Principle should be "if you have a subtype P of type S, redesign your code so that there are no subtypes". Seriously though, I have been a professional developer in a classical object oriented language for the last 5 years and have written 0 class hiarchies. Common functionality can be mixed in by composing "parent" objects around common child objects. Passing multiple types of objects to a single function is better handled via interfaces, not inheritance. Is there a good reason to have a class hierarchy in modern programming?
- thisiszilff 6y agoTo an extent that is what it ends up being 90% of the time given that a lot of the inheritance people use don't satisfy the LSP (Liskov Substitution Principle). We've been using mypy with our python codebase and it will complain if you violate the LSP so much so that it prevents most inheritance. We mainly subclass for simple data models when it's a case of "every model has these 10 fields" and there are next to no methods on the data model classes.
- mumblemumble 6y agoYou're contradicting yourself a bit here. Implementing an interface is just a special case of inheritance. The fundamental reason why some languages make a formal distinction between interfaces and purely abstract base classes is that using the "interface" keyword is effectively signing a contract that says you will not do this one thing (implementing virtual methods), and in return the compiler will allow that base type to participate in multiple inheritance. edit: I should also mention that it's entirely possible to violate LSP using only interface inheritance. Java's collections do this: Collections.unmodifiableList() returns a List<T> that can't be modified. But List<T> is rotten with methods for mutating the list. So what happens if you try to mutate one of these? You get a rather surprising exception. I suppose you could argue that this isn't technically a violation of LSP, because the JavaDoc for List<T>::add says that it might throw UnsupportedOperationException. But UnsupportedOperationException is a RuntimeException, not a checked exception, and it's rare for Java developers to make a habit of handling UnsupportedOperationException when mutating collections. So I'd argue that it's only a part of the method's spec in the most pedantic of ways. IMO, the more pragmatic perspective is that creating a subtype that inherits a whole bunch of interface methods that it has no intention of implementing is just about as clear-cut a violation of LSP as you can get.
- evdev 6y agoMeta comment, but I would love for there to be more insight into the sociology of Hacker News. I feel like in the last couple of years we've seen a stark decrease in the frequency of "What to expect when you're expecting (to be a millionaire founder)" articles and a stark increase in "Enterprise Patterns" discourse. The latter is oddly at pretty serious tension with PG's ideas about development and Scheme. I find the "Liskov Substitution Principle" is an awkward way of pointing at the idea that your usage of subtyping should not render the type variance in your system nonsensical.
- Jtsummers 6y agoIt's cyclic. When I first joined over a decade ago there seemed to be a lot of technical posts and discussion at the time, which gave way to a lot of SEO discussions, which gave way to technical discussions, then shit tons of Erlang (not that I minded), then other stuff for founders, etc. People tend to post interesting things which leads to other interesting and related things. So you'll see a lot of a certain topic or field, maybe a surge in new users or user activity because it interests them, and then some new topic or field becomes dominant for a while and appeals to a different subset of users who become a bit more active with submitting, posting, and voting.
- toolslive 6y agoIn golang, X is a subtype of interface{}. But []X isn't a subtype of []interface{} (according to the compiler). Doesn't this violate the above subsitution principle ?
- zozbot234 6y agoIn golang, a white horse is a horse: If you ask me for a horse, I can give you a white horse. But if I want a stable for n horses, it's not correct to give me a stable for n white horses, because that doesn't account for yellow or black horses. Does this violate the substitution principle? (inb4: "It depends on what the meaning of 'is', is!")
- scubbo 6y agoAh, this sounds like co/contravariance - concepts that I have encountered, (re-)learned, and forgotten countless times.
- toolslive 6y agoTo continue your analogy, I want to be able to see the stable of white horses ([]X) as a stable of horses ([]interface{}), since a white horse is a horse after all. In java (or C++ or most other languages), you can do the following String[] strings = {"x0","x1"}; Object[] things = strings; They can't be both right.
- Tyr42 6y agoThat example with arrays only works if you treat the array as immutable. Otherwise you get an exception.
- toolslive 6y agoString[] strings = {"x0","x1"}; strings[0] = "something else"; // mutable. Object[] things = strings; things[1] = null;// mutable. all works.(in java), but I understand what you mean.
- Fatalist_ma 6y agoI don't think this is a good or even valid example. Where do we see that the method that depends on the Rectangle class no longer works correctly for instances of Square? Why does it depend on the exact value returned by the object? The example from the Stackoverflow link makes more sense: " ssert.AreEqual(20, rect.Height); ... because changing a rectangle's width does not affect its height. " Well, we may choose to not follow that assumption in our project. Let's say we're making a graphical editor, our rectangle could have various restrictions - like fixed proportions, max/min area, etc; so I can imagine how we may not have a guarantee that changing 1 side does not change another side. If it's just for general-purpose geometric calculations - then sure, but then it better be an immutable object. I mean, you should not use inheritance for things like these but I don't think this always violates LSP.
- jrockway 6y agoI have always felt that people use OO exactly the opposite as Liskov suggests. Liskov says that subclasses must be fully substitutable, and that implies that any valid operation on the superclass must be valid on the subclass. So say you have a Bag class. You can add elements to it at will. Then you create a Set subclass, and now inserting a duplicate throws an exception. You just broke substitutability. Even though the code is easy express with this hierarchy (if exists(element) throw else super(element)), your program blows up if you try to substitute. Doing the opposite is safe, of course, but the code reuse doesn't work when you make your Bag a subtype of Set. From an algorithm standpoint, a Bag isn't really implemented in terms of a Set, so you're writing a new data structure from scratch (rather than just changing one condition in insert() like the invalid example). What I take away from this is that people are mostly using OOP to cut-n-paste code when two things are similar. I see the value in not wanting to rewrite code (or to create a third "common factor" that you will never use, to implement Set and Bag), but I think this use of OOP basically precludes any computer science reasoning when looking at your classes. Apparently that's not a problem, as people are ignoring Liskov right and left while building good stuff. But thinking about it deeper, I fear that we didn't find the right model when we invented OOP, and that is why newer languages are ignoring OOP in favor of composition.
- narag 6y agoFor some people, programming languages are much like math, so there's right and wrong, and there's a right way to do OOP, or even OOP is or isn't right in itself. Maybe I'm wrong, but I tend to see language features as a usability question. In different human languages (or even in the same language) you can say the same thing using different syntax, but one way of saying it is more clear or more inspiring. It's not what some object is, it's how it works better what makes sense to me. Again, that's just me, YMMV.
- mumblemumble 6y agoI agree, it's better to be pragmatic. So it's not super useful to say that LSP is about whether you're doing it right or wrong. I prefer to think of it in terms of making the codebase intuitive to work in. So I like to formulate it as, "You should be able to safely and fearlessly plug any subtype of a class into a method that accepts that class as an argument." This is a bit of a departure from how Liskov originally formulated things, but I think it is fair to say that it's a refinement that better captures the spirit of how she talks about it in more recent interviews. And it also captures why it's such an important principle, especially if you're working on a team: Nobody wants to get stuck working in a codebase where Widgets are partitioned into two different categories that can only be distinguished by carefully reading their implementations, according to criteria that can only be understood by carefully reading the implementations of methods that operate on Widgets. It's not wrong, per se, it's just not a working environment that any self-respecting person wants to be stuck in..