Saturday, August 16, 2008

The legacy of inheritance

Is inheritance really useful or is it a feature that causes more problems than it solves? Certainly I can't think of a case where I've been really grateful that I've been able to inherit from a superclass but I can think of several instances where it has caused friction and where the extension mechanism of inheritance tended to lead the programmer the wrong way.

Consider the following code (public fields for brevity):

public class Square {
  public int base;
  public int getArea() {
return base * base;
 }
}

public class Rectangle extends Square {
  public int height;
  @Override public int getArea() {
    return base * height;
  }
}

public class Parallelogram extends Rectangle {
  public double angle;
}

Now how do we implement a Rhombus? Is it a Square extended with an angle (and overridden area calculation) or a Parallelogram with conditions on setting the properties so that the invariant is preserved (which is why we should have accessors, by the way)?

Well, the correct answer is neither, even though we have a nice sequence of extensions. The problem is that we have been led astray by the extension mechanism and violated the "is a" rule for subclassing and ended up with a corrupt type system. Clearly a Parallelogram is not a Rectangle which equally clearly is not a Square so a subclass instance may not safely be used in place of a superclass instance. Reversing the class hierarchy solves the issue, however it creates subclasses that are restrictions of the superclass rather than extensions.

An acquaintance of mine who is highly experienced in creating standardized components for information interchange once claimed that it only works to inherit properties by restriction. This is well worth considering, although I'm not entirely convinced because it causes a different set of problems when restriction means "that particular property is not used", as it so often does in information interchange scenarios. In that case it is usually not interesting nor useful to know what the superclass is. In our case at hand it will work because the subclasses actually have a meaningful and useful value of the property but it is restricted by an invariant, so it is also useful to be able to view them as instances of the superclass.

Let's try coding again:

public class Parallelogram {
  protected int base;
  protected int height;
  protected double angle;

  public void setAngle(double angle) {
    this.angle = angle;
}
}

public class Rectangle extends Parallelogram {
  @Override public void setAngle(double angle) {
     // What should we do here?
  }
}
...

Oops, this doesn't seem to work well, either. Obviously we can't just inherit the setAngle method or we could end up with a corrupt Rectangle, nor is there any reasonable action we can take. We can get out of our pickle, however, because constructors are not inherited! Simply make our instances immutable:
public class Parallelogram {
  public final int base;
  public final int height;
  public final int angle;

  public Parallelogram(int base, int height, double angle) {
    this.base = base;
    this.height = height;
    this.angle = angle;
  }

  public int getArea() {
    return base * height;
  }
}

public class Rectangle extends Parallelogram {
  public Rectangle(int base, int height) {
    super(base, height, Math.PI/2);
}
}

public class Square extends Rectangle {
  public Square(int side) {
    super(side, side);
  }
}

That seems to work, and the implementation of getArea() is actually profitably inherited. But we still have a problem in Java with a Rhombus, since a Square is both a Rectangle and a Rhombus but a Rhombus is not a Rectangle. To solve that would require defining the types as interfaces and have separate implementation classes (now I understand why that is often  recommended :-) ).

If we had chosen to implement Parallelogram with a field for the length of the side instead of the height, the formula for the area would have been base*Math.sin(angle)*side. Even if this would have worked for our Rectangle, it would have been inefficient (although when the fields are immutable it could probably be optimized by the compiler and/or JVM).

On the whole, I believe it is seldom profitable to inherit the implementation of a method, if you have a good example to the contrary, please share it.

I think that overriding all public methods would be preferable, even if you just decide to call super's version, at least it would show that you gave some thought to whether it was correct or not. Without having done an exhaustive study, I believe this is especially true for interfaces that indicate that a class "is" something rather than that it "is a" something, i.e. those interfaces whose name usually end in "able". Try Cloneable, Externalizable, Runnable and Printable.

2 comments:

Doug said...

In my opinion, the basic problem isn't inheritance. It's the expectation of substitutability.

On the other hand, Bertrand Meyer is a lot smarter than I am, and he writes, "a number of authors have suggested separating between module inheritance, essentially a tool to reuse existing features in a new module, and type inheritance, essentially a type classification mechanism. Such a division seems to cause more harm than good, for several reasons."

But then, Meyer's Eiffel language has a lot more powerful inheritance mechanism than most OO languages do.

Back to my original point… most real-world object "classes" are not fully substitutable, and attempting to define them to be substitutable is folly. In my opinion, substitutability is best reserved for classes of our own design.

Bad Wolf said...

What you have done here is set up a 'straw man' logical fallacy. http://en.wikipedia.org/wiki/Straw_man