July 11, 2007
Is an Object-Oriented Square a Rectangle?I was in a course recently and someone said that it was wrong to have a class hierarchy where you had a base Rectangle class, and then you had a Square class that inherited from Rectangle.
Now, I know that in general people went too hog-wild with inheritance when they first started using object-oriented programming. The canonical wisdom from the Design Patterns book is that if you have a class for an encryption chip and then you have different kinds of encryptors, rather than have a base EncryptionChip class and then have AesEncryptionChip and Rot13EncryptionChip and etc, all of which inherit from EncryptionChip, you should instead design EncryptionChip to contain an Encryptor class which does the actual encryption (this is know as containment or aggregation). That way you can construct an EncryptionChip with the specific Encryptor you need, but the specific Encryptor doesn't need to know anything about the EncryptionChip (as opposed to a subclass, which does need to know about its parent class). In OO terms they are more loosely coupled, which means it's easier to make changes if needed. If you design the Encryptor class so that EncryptionChip only deals with an abstract base class, and the concrete Encryptors inherit from that base class, then you have the design pattern known as the Strategy.
But in the case of Square inheriting from Rectangle, I don't see what is wrong. I suppose you could have the Rectangle class have a member that did some sort of ValidateSize() function, which for the Square would fail if the x and y dimensions were unequal, but that seems over-designed. Anyway, the argument against Square inheriting from Rectangle was not about the internals of implementation, but rather about the external behavior that this Rectangle-that-is-really-a-Square would present to the world.
The argument specifically relates to the Liskov Substitution Principle. The LSP informally states that if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering the program. In this specific case, if Square is a subtype of Rectangle, then objects of type Rectangle in a program may be replaced with objects of type Square without altering the program.
At first glance this doesn't seem like a problem for Square. After all if you are using a Rectangle somewhere and you replace it with a Square (which inherits from Rectangle and thus supports at least the same public interface) then everything should work. And this seems "right", because in a mathematical sense a square IS a rectangle, so an object-oriented design rule that disallows a Square from being a Rectangle would seem to indicate a bad rule.
But, the Liskov argument against Square : Rectangle is that if you were to set the parameters of something you thought was a Rectangle, and you happened to set x != y, then it would work for a base Rectangle, but not for a Square (since the Square presumably fails in some way in that situation). Thus, designing Square to inherit from Rectangle is wrong because a Square CANNOT be substituted for a Rectangle.
I'm not buying it. First of all, as I said, a square is a rectangle. But also, if you dig deeper into Liskov (meaning you read the Wikipedia entry), it defines it formally as:
Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.
And to me there is nothing in there that disallows Square from inheriting from Rectangle. Everything that could provably be true about a rectangle is certainly true about a square. I don't consider "Well, x and y MAY be different" to be a provable property--a rectangle says nothing about the relationship between x and y, so there is nothing for a square to contradict.
Further, if you look at Liskov from a Design by Contract perspective, it states that preconditions cannot be strengthened in a subclass, and postconditions cannot be weakened. Which is fine: in moving from Rectangle to Square you are strengthening a postcondition (by adding one that says x must equal y).
The book The Pragmatic Programmer also briefly touches on Liskov, defining it (on pp. 111-112) as:
Subclasses must be usable through the base class interface without the need for the user to know the difference.
I don't see a problem here; you would certainly be able to use a Square through the Rectangle base class. I mean, your Rectangle.SetXY() method might fail, but any method you call might fail; if some code handed a Square off to other code that was using it as a Rectangle, it's presumably because they wanted something to fail in that situation (while allowing that other code to remain loosely coupled to themselves). The Pragmatic Programmer also says (in reference to Liskov and DBC) "A subclass may, optionally, accept a wider range of input, or make strong guarantees." Which is exactly what a Square is doing, making stronger guarantees.
So, my conclusion is that the Liskov argument, in this situation, is wrong, and (at least from that perspective) it's perfectly fine for Square to inherit from Rectangle. The amazing thing, of course, is that all this is so unclear, so many years after object-oriented programming came about.
Posted by AdamBa at July 11, 2007 12:11 PM
TrackBack URL for this entry:
Square and rectangle are both types of shape. Now consider a shape object. It really just has a count of the number of sides, plus a length for each of those sides. Which is fine.
The problem with square and rectangle is the specialisation; i.e. people want to set Height and Width properties, which are not applicable to shapes. The rules still apply, you can use the base methods/interface. That's where the problem lies; people forget about the base class. They want the specialisation, they want the short cuts, and that's why they want to just set a side length once for a square.
Posted by: BarryD at July 11, 2007 02:00 PM
OK, so somebody defines a new method on Square that lets you set the width and height by passing a single value. If someone knows an object is a Square they may call this method (and it will work) and if they are treating it as a Rectangle, or Shape, they won't know the method exists, so they won't try to call it.
Posted by: Adam Barr at July 11, 2007 10:24 PM
I'm sooo not buying this.
Rectangle rect = Factory.getQuadrangle();
What does rect.getArea() return now?
And what if getShape() had returned a Square?
Posted by: Isaac at July 11, 2007 11:02 PM
Can you explain your scenario a bit more? What are you not buying?
If you mean that rect is really a Square, then what happens depends on how Square is implemented. For example setWidth and setHeight may both silently set width AND height, so getArea would return 25 given that 5 was the last value passed in. Or maybe they throw an exception if width and height don't agree.
(I mean, this is object-oriented code right? If there were ever two people in the world who might have different ideas about how to implement an interface than all this encapsulation might be a problem. Luckily that NEVER happens so it's all crystal-clear.)
Posted by: Adam Barr at July 11, 2007 11:29 PM
When I took HS geometry (about 55 years ago), squares weren't rectangles, neither were they rhombuses, scalene triangles weren't isosceles, isosceles triangles weren't equilateral and a line was not parallel to itself. I realize that all these conventions were wrong. A class shold not, with rare exceptions, be defined by exclusing a natural class.
Now if rectangle has setheight and setwidth functions, the square class should implement them both, but additionally set the other the same. So if you setheight to 4 and then setwidth to 5, the result should be to have both height and width set to 5.
Posted by: marble chair at July 13, 2007 10:07 AM