4 ms·
BTW 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.
by sumanthvepa 6y ago
BTW 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.