A colleague and I wrote a paper for a computer-science education conference about the Shape hierarchy. Basically, it's a very evil hierarchy to teach to students because a naive approach to the hierarchy based on mathematical definitions results in bad object-oriented design. Unfortunately for me and my colleague, our paper was rejected, so I'm going to take some of the ideas from that paper and play around with them on this blog.
Let me first explain the problem. A high-school math teacher tells us that "a rectangle is a shape..." and "a square is a rectangle...". A computer-science teacher tells us that "is a" means class inheritance. So (in Java):
abstract class Shape { }
class Rectangle extends Shape { }
class Square extends Rectangle { }
This glosses over the data-representation issues which I'll (hopefully) talk about in another blog post. I want to focus first on the primary problem: squares behave differently than rectangles, and squares cannot do all the same things as a rectangle.
Consider a resize(double x, double y) method; this should rescale any Shape in the x and y dimensions. We drop this in the Shape class, Rectangle implements it, and Square gets it for free!
Now let's pretend that s is a Square. Is s still a Square after s.resize(4.0, 0.5)? According to Java, yes, it is; according to the mathematics, no!
Whatever we can do to a Shape or Rectangle, we need to be able to do to a Square, no questions asked. This is what the inheritance says. And it's no fair throwing an exception or somehow adapting the arguments in Square.resize(double,double). That's not what the method is supposed to do. (Less importantly, it also means Square would no longer get an implementation of resize(double,double) for free.)
One reviewer of our paper suggested that Square should have a resize(double) method. Yes, you can do this, but to what purpose? The whole idea behind the hierarchy, presumably, is that some program is going to be using Shape objects. At that level, you need to be able to do resize(double,double) on any and all Shapes.
This is the core, technical reason why Square should not extend Rectangle. If you'd like to know more, look for its more technical name: the Liskov Substitution Principle. There are other reasons, some minor, some not so minor, why Square does not extend Rectangle; I'll talk about these in future blog posts.

2 comments:
OK, so I'm idling away on Blogger instead of doing Productive Work(tm) but I was intrigued by this.
The right thing to do here would be to get the kids all settled with "Square isa Rectangle" and then blow their minds by saying something like "ah, but what happens when Rectangle is extended? Perhaps square should be hung on a different branch of the Shape family tree." RegularShape, perhaps, which could also encompass things like Circle (not an Ellipse!) Convenient examples often break down under the weight of real life problems (he says, redoing the /perfect/ data model for the third time in as many years.)
This is where more advanced modeling techniques come in handy. For example each shape could expose an object that can be used to change the dimensions of that shape called something like SizeManager which could then be extended by each class to suit the needs of that class. This way if there are any common behavioral requirements of a SizeManager that could be placed in that abstract base class.
Another thought is that shapes may be thought of as immutable in which you cannot resize them but only produce new shapes in their place. This pattern side-steps the resize issue but only requires that one would be able to produce a new shape with the required dimensions.
Another way to think if it is that just specific attributes of a shape are modeled as objects so that you have something like RectangleConstraint and SquareConstraint where SquareConstraint inherits from RectangleConstraint. Now you can have a generic Shape object or perhaps which represents a closed geometric shape and that owns a Constraints object which would vary along the different shape instances. This approach could even support mutability if one were to assume that the Constraint could vary dynamically using something such as the State pattern.
See the main problem with the real world example is that it is being approached in a way that assumes that every attribute of a rectangle is attributable to a square which just isn't true especially when you include the idea that a shape's size is mutable.
There are probably several other examples that could help to hold up the idea that Square does in fact inherit some attributes from a Rectangle.
Post a Comment