Sunday, December 13, 2009

Inheritance nightmares

I was first exposed to object-oriented programming when learning C++. I am now starting to realize how confusing that has been. In particular, inheritance in C++ is often used and abused to implement various kinds of relationships between classes. I am writing "abused", because the only legitimate use of inheritance is for IS-A relationships. Other relationships such as HAS-A and IS-IMPLEMENTED-IN-TERMS-OF should not use inheritance. In these cases composition is preferable. It is therefore important to be able to differentiate between the different kinds of relationships.

However understanding IS-A relationships is not as easy it might seem. To illustrate the problem, the case of squares and rectangles is sometimes used.

All squares are also rectangles, and it seems natural that a class "Square" should be related to a class "Rectangle": Square IS-A Rectangle.

In C#, this could be written:

class Rectangle
{
}

class Square : Rectangle
{
}


Now that we have our class hierarchy, let us start adding methods and fields:


class Rectangle
{
float length;
float width;

public Rectangle(float l, float w)
{
length = l;
width = w;
}

public float GetLength() { return length; }
public float GetWidth() { return width; }
}


So far, no problem. Things get tricky when we want to add setter methods:


class Rectangle
{
float length;
float width;
...
public void SetLength(float l) { length = l; }
public void SetWidth(float w) { width = w; }
}

class Square : Rectangle
{
...
public void SetSize(float s) { SetLength(s); SetWidth(s); }
}


The problem is that Square inherits SetLength and SetWidth, which makes it possible to have squares with non-matching lengths and widths. A non-satisfactory solution consists of overriding SetLength and SetWidth in Square:


class Rectangle
{
float length;
float width;
...
public virtual void SetLength(float l) { length = l; }
public virtual void SetWidth(float w) { width = w; }
}

class Square : Rectangle
{
...
public override void SetLength(float s) { base.SetLength(s); base.SetWidth(s); }
public override void SetWidth(float s) { base.SetLength(s); base.SetWidth(s); }
}


This solution is not satisfactory because users of Rectangle may expect that it should be possible to set widths and lengths independently.


void SetWidthAndLength(Rectangle rect)
{
rect.SetWidth(1.0);
rect.SetLength(2.0);
Debug.Assert(rect.GetWidth() == 1.0);
Debug.Assert(rect.GetLength() == 2.0);
}


These assertions fail when SetWidthAndLength is passed an instance of Square. Many articles go on by stating that Rectangle and Square are in fact not IS-A related, although the initial intuition told otherwise. This is usually the point where the Liskov Substitution Principle is introduced. While understanding this principle is certainly a good thing for anyone learning about object-oriented programming, one of the biggest selling points of object-oriented design is that it is supposed to be intuitive.

After all, all squares are rectangles, aren't they? Indeed, this is true of immutable squares and rectangles. Remove the setters, and all problems disappear. The substitution principle falls in line with what intuition tells us.

Going a bit further, let us consider a function which transforms squares.


Square TransformSquare(Square square, SquareScaling scale)
{
float scaleFactor = ...;
return scale.Do(square, scaleFactor);
}


If we have at hand a library which provides translation and rotation operations on rectangles, we are very close to being able to use these with "Transform". All we need is to write a bit of code to turn rectangles which have identical lengths and widths into squares:


class RectangleUniformScalingAdapter : SquareScaling
{
RectangleUniformScaling adaptee;
...
public override Square Do(Square square, float factor)
{
Rectangle tmp = adaptee.Do(square, factor, factor);
Debug.Assert(tmp.GetLength() == tmp.GetWidth());
return new Square(tmp.GetLength());
}
}


On the other hand, if we are provided a function working on rectangles, a library providing transformations on squares would not be very valuable.

In other words, we could say that RectangleUniformScaling IS-(almost)-A SquareScaling. Note how the IS-A relationship is reversed.

If different parts of the interfaces of Rectangle and Square do not agree on the direction of the relationship, we have a problem.


class Rectangle
{
float width;
float length;

public virtual float GetWidth() { /* Easy to implement */ }
// Uniform scaling
public virtual void Scale(float scale) { /* could use Square.Scale() */ }
public virtual void Scale(float scaleWidth, scaleLength) {...}
}

class Square
{
public virtual float GetWidth() {/* could use Rectangle.GetWidth() */ }
public virtual void Scale(float scale) { /* Easy to implement */ }
public virtual void Scale(float scaleWidth, scaleLength) { /* Impossible to implement */ }
}


Who should inherit from who?

At this point, readers interested in F# and functional programming languages might wonder what all this has got to do with F#. I think that the common mix of mutability and inheritance is not a very strong basis for good software design. I never really realized that until I took a look at functional programming and immutability.

1 comment:

Markus said...

Nice post. I find learning about functional programming is making me a better OO-programmer as well. When you actually think in terms of mutability of state when designing classes you tend to take it a step further.