Reputation: 29
I came across this example when I was trying to learn more about LSP, I am new to the OOPs world. You can find my question after the example. The example is as follows.
In mathematics, a Square
is a Rectangle
. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square
derive from Rectangle
, then a Square
should be usable anywhere you expect a Rectangle
. This makes for some strange behavior.
Imagine you had setWidth
and setHeight
methods on your Rectangle
base class; this seems perfectly logical. However if your Rectangle
reference pointed to a Square
, then setWidth
and setHeight
don't make sense because setting one would change the other to match it. In this case Square
fails the Liskov Substitution Test with Rectangle
and the abstraction of having Square
inherit from Rectangle
is a bad one.
My question is : What kind of methods are we talking about here? Are we talking about virtual methods that can be overriden? If not, I don't see a scenario where the derived class can provide it's own implementation. In the example mentioned above, if both the methods setWidth
and setHeight
are non virtual methods that belongs to the base class and it sets the width and height respectively , then these methods should work just fine. So is it about standards and not about the methods working as expected? By standard I mean , having a method setWidth
and setHeight
accessible from a square object doesn't make sense eventhough it works.
I did not try anything , I ran into this confusion and thought I'd look for some answers.
Upvotes: 2
Views: 49
Reputation: 183514
In math, objects like "rectangles" and "squares" are immutable; there's no concept of taking an existing "rectangle" and changing its dimensions, because its dimensions are an intrinsic aspect of the rectangle. A rectangle with a different width is not the same rectangle anymore.
That doesn't mean that you shouldn't create a Rectangle
class with a setWidth
method; it's just that the instances of that class won't represent "rectangles" in the mathematical sense. That's fine and normal; if I say "The key cards are plastic rectangles", I'm not using "rectangle" in the strict mathematical sense, I'm just describing the key cards' general shape. (In reality they probably have rounded corners, and certainly have some three-dimensional thickness, and are presumably somewhat flexible, and so on.) So the question is what you actually need to represent in your program. I notice that you mention height
rather than length
, which makes me think this is a rectangle drawn on a computer screen with perfectly horizontal and vertical edges; if so, then depending on the details of your program, you may want not just height
and width
but also things like top
, left
, zIndex
, borderStyle
, borderWidth
, borderColor
, fillStyle
, fillColor
, and so on. Many of those make no sense in a typical mathematical domain, but if they make sense in your problem domain, then that's what matters.
Whether to have a Square
class or not is, likewise, a question for your problem domain. In a drawing program, I wouldn't really expect "squares" to be a distinct shape where I can change the side length but not the aspect ratio; so if what you have is a drawing program, then I'm guessing you don't need a Square
class. But only you can decide that. If you do have a Square
class, and your Rectangle
instances' widths and heights can be changed independently, then your Square
class should not be a subclass of Rectangle
, for exactly the reason you say.
(In math, for what it's worth, a square is not necessarily a distinct "type" in this way. After all, every rectangle with width = length is a square; so sometimes you specifically construct a square, but other times you construct a rectangle and it turns out to be a square. But the considerations in object-oriented programming are completely different, and while the mathematical objects may be helpful to think about when figuring out your class design, they generally won't govern your class design.)
Upvotes: 0
Reputation: 73530
The key here is that LSP is about the contract and what this contract guarantees. It's not about the code.
Many LSP-compliant designs allow Square
to be a specialisation of Rectangle
. Take for example this design, where the shapes are immutable, and transformations only return a new shape without making any promises about what that shape could be:
Everywhere, where an instance of Shape
or Rectangle
is used, an instance of Square
could be used instead. Keep in mind, in this regard that constructors are excluded from LSP constraints, according to Liskov & Wing who are at the origin of this principle:
Objects come into existence and get their initial values through creators. Unlike other kinds of methods, creators do not belong to particular objects, but rather are independent operations.
However, slight alterations to such an LSP-compliant design could easily break LSP. For example, adding the operation changeSize(int X, int Y)
would no longer allow to use a square everywhere are rectange is used: every time the size would be changed differently on the X and the Y axis, the square's invariant would be broken, and so an operation valid for the rectangle would not work for the square.
Your example is slightly different. LSP compliance will depend on the contract that you set on setWidth()
(and setHeight()
likewise): if the postcondition is that width
has the new value and height
is unchanged, then there is infringement. But is this the promise you made in your contract? If the post-condition is only about width
and makes no guarantee that height
stays unchanged, the design would still be LSP compliant.
The LSP is in reality not defined in terms of methods, but in terms of provable predicates (based on contracts). So it makes no difference if we're talking about virtual or ordinary member functions. Many OOP languages do not even make this difference and have all functions be overridable. Nevertheless, in the case of C++, if you make a polymorphic design, where subtypes are meant to be used instead of super types, it's a good reflex to build the class hierarchy and make methods virtual (see for instance this C++ core guideline).
(Note by the way that if your design allows to use non-virtual setWidth()' and setHeight()
of a rectangle to alter the object which could be a square, it's a bad polymorphic design, independently of LSP).
Upvotes: 0