In my previous post, I’ve used the example of squares and rectangles to illustrate the importance of definitions. It is clear that, from a geometric point-of-view, a square is indeed a kind of rectangle. However, as my friend Arnon has noticed, this may be problematic from an object-oriented perspective.
Let’s say that we organize the classes Square and Rectangle in a hierarchy so that Square inherits from Rectangle. This reflects exactly the geometric relationship between them, since inheritance is commonly used to represent the is-a-kind-of relationship.
Now suppose that we have a photo-enhancement application with a feature to take a picture and place a border around it. It would be natural to use a Rectangle as the border, and have a function that would receive as parameters the original Picture and a Rectangle and return a new Picture with a border:
Picture addBorder(Picture picture, Rectangle border)
This function would be responsible for adjusting the height and width of the border to fit the dimensions of the picture.
What is the problem now? Because Square is a subclass of Rectangle, we could pass an instance of Square as the border parameter. But then, if the picture is not a square, it should not be possible to change the border’s height and width, or otherwise the border would no longer be a square.
The conclusion is that a square cannot be used in every situation in which a rectangle can be used, and therefore the class Square should not be a subclass of Rectangle.
But what is the root of the problem?
In the world of geometry, figures are static objects with unchangeable properties. But in the world of object-oriented programming (OOP) objects also have functionality that implement a well-defined dynamic behavior.
Structurally, Squares are a specialization of Rectangles, since both have four sides and all internal angles right. A Square is special because it has its height equal to its width.
Functionally, Squares do not behave like Rectangles. A Rectangle should have independent methods to change its height or its width. Clearly, these methods would not be appropriate for a Square.
Inheritance should be used only if the is-a relationship is both structural and functional.
I believe this argument – why square is not rectangle and circle is not ellipse – dates back at least 15 years ago. But it is nice to have each generation re-discover the light. It certainly says something in favor of the light 🙂
Originally, they used this example to demonstrate the principle of substitutability (but it may be even older).
Click to access lsp.pdf
A Square is indeed a specialized Rectangle, and a specialized Lozenge as well.
(p => q) doesn’t mean that (q => p), it just mean that (not q => not p). Similarly in the example of addBorder method:
use-case 1: if the picture is a square, the rectangle will end up to be a square once find out by the addBorder method, then Square is indeed a specialization of a Rectangle
use-case 2: if the user wants to pass a Square (as a specialization of a Rectangle), he should be able to because some frame are squares. The method will indeed be responsible to fold a piece of the picture… which piece will be hidden: horizontally or vertically? maybe an optional parameter is missing…
The Object are there, they have been designed independently of their usage. The method signature is the contract requested by the user, if he wants a true Rectangle and nothing else, maybe a TrueRectangle class is missing in the hierarchical definition tree (even graph), or a keyword in the language may be missing (ala constraint genericity in Eiffel).
To go beyond: post-condition should ensure that picture.height = border.height and picture.width = border.width. With the Substitution Principle presented in another post, it will be obvious that the post-condition will crash exhibiting a valid case which has not been considered in the design of addBorder method. The engineer will have to update the method either with TrueRectangle or with optional arguments to handle Square as well.
What we can see here is the importance of a clean and clear unit test strategy.
OOP/OOD is driven by engineers, they are not magic methodologies to get rid off human brain. The beauty of separating data structure graph and usage is helping the designer to browse more use-cases he potentially primarily didn’t expect…
In conclusion, I second the standpoint stating that a Square is a Rectangle (and a Lozenge).
Very informative. The dynamism of objects and this special case has broughtout the limitation of validity of classification. So, what is the new principle of classification which works for both dynamic and static objects?