[ This is an ongoing discussion of inheritance reflected in a debate
over rectangles and squares. See also <subtyp> and <square> ]
RESPONSE:
rmartin@thor.Rational.COM (Bob Martin), 17 Mar 93
[...]
Often we deal with mathematical concepts, and find that they don't
jibe quite right. The Square/Rectangle issue is just one of many such
connundrums.
Several people suggested that, by the above definition, A rectangle
ISA square since anything that can be done to a square can also be
done to a rectangle.
But John Skaller said it best when he noted that it depends upon what
you are trying to do.
For example:
class Rectangle
{
public:
Rectangle(double height, double width)
: itsHeight(height)
, itsWidth(width)
{}
double Area() const {return itsHeight * itsWidth;}
private:
double itsHeight, itsWidth;
};
class Square : public Rectangle
{
public:
Square(double side)
: Rectangle(side, side)
{}
};
There is nothing wrong with this; other than that Square is not as
dense as it could be. In this case, both the square and
the rectangle are merely descriptive. Thus, the Square ISA Rectangle.
However, if we add:
void Rectangle::Stretch(double width, double height)
{
itsHeight *= height;
itsWidth *= width;
}
Now we have a problem. Square simply cannot afford to inherit this
function since its invocation could place the square in an state which
is not compatible with being a square. (Its sides could be made unequal)
Thus, Square can no longer sustain the ISA relationship with
Rectangle...
Should we turn it around??? Should Rectangle derive from Square.
Consider the following:
class Square
{
public:
Square(double side)
: itsSide(side)
{}
virtual double Area() const {return itsSide*itsSide;}
protected:
double itsSide;
};
class Rectangle
{
public:
Rectangle(double width, double height)
: Square(width) // ICK!
: itsHeight(height)
{}
virtual double Area() const {return itsSide * itsHeight;} // ICK!
void Stretch(double height, double width)
{
itsSide *= width; // ICKY ICKY!!
itsHeight *= height;
}
private:
double itsHeight;
};
This is revolting. We are using a member variable of Square for a
purpose it was never intended to serve. itsSide was supposed to
represent the length of the side of a Square, but objects of class
rectangle are using it to represent the width of the rectangle.
This is a kludge, and will be a source of future errors.
In support of the kludge theory was the necessity of making the member
variable "protected". This, in a way, admited that definition of the
variable was ambiguous.
Finally, reading the code is not easy since one must know that a
"trick" is being played on class Square before the Rectangle code can
be properly understood.
=-=-=-=-=-=-=-=-
So what is the best model? It depends. I would separate these two
objects completely. Perhaps by deriving them from the following
abstract base:
class ObjectWithArea
{
public:
virtual double Area() const = 0;
};
class Square : public ObjectWithArea {...};
class Rectangle : public ObjectWithArea {...};
But like John said, it depends upon what you are trying to do.