nandu
nandu

Reputation: 29

LSP and virtual methods

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

Answers (2)

ruakh
ruakh

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

Christophe
Christophe

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:

UML class diagram with Square inheriting from Rectangle inheriting from abstract class Shape

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

Related Questions