Avoid extending classes

Arthur J. Riel argued in his 1996 book "Object-oriented Design Heuristics" that "All base classes should be abstract classes." He makes the argument largely by showing the consequences of behaving otherwise. Simple enhancements of the code may require renaming or replacing classes globally. Even then, it seemed reasonable to restrict the preference further to "pure abstract base classes" or "interfaces." It is an easy heuristic to follow, with few inconveniences. Violations are usually justified in the name of efficiency.

After another decade of using C++ and Java, I've found myself following an even harsher heuristic: whenever possible, avoid extending any concrete class. In Java, this would amount to making every class final. There are important exceptions to this rule, but attempting to follow it has led to much more robust and easily modified code.

First of all, we have a now common preference for containment over inheritance. We do not extend a class just to share an implementation. A container can access an implementation without having to be useful everywhere the contained object would be.

In "Effective C++ Programming" (originally 1996), Scott Meyers made useful distinctions with several rules: "Make sure public inheritance models 'isa'." "Differentiate between inheritance of interface and inheritance of implementation." "Model 'has-a' or 'is-implemented-in-terms-of' through containment."

Implementing an interface is pure polymorphism. No implementation details need to be shared between classes sharing the interface.

If you have multiple classes supporting the same protocol or group of methods, would you not define a shared interface? If you have a shared interface, do you still need to use inheritance to share implementation? Could you not share implementations by containment?

There is no single standard design pattern for sharing implementation by containment. I use a pattern that I'll call an interface decorator, because it resembles the decorator pattern limited to interfaces. But it also resembles the bridge and composite patterns, so let us not not worry about the name.

Here is an example in Java. First let's begin with an interface that contains three methods
public interface AnInterface {

  /** Changes state in some way */
  public void method1();

  /** Takes a value and returns a value */
  public int method2(int value);

  /** Returns a value */
  public int method3();
}

Now we create a class that implements that interface and contains an instance of that interface.
public class InterfaceDecorator implements AnInterface {

  private final AnInterface containedInterface;

  /** Constructor wraps an instance of the same interface */
  public InterfaceDecorator(AnInterface containedInterface) {
    this.containedInterface = containedInterface;
  }

  // Replace inheritance by delegating to contained implementation 
  public void method1() {
    containedInterface.method1();
  }

  // Modify implementation.
  public int method2(int value) {
    value += 1;
    return containedInterface.method2(value) + 1;
  }

  // Completely change implementation.
  public int method3() {
    return 5;  // ignoring contained implementation
  }

  // Convenience method using other existing methods.
  public int convenienceMethod() {
    method1();
    return method2(7);
  }

}
Some methods have the same behavior as before, some are modified, and some are entirely new.

This class effectively extends any instance of an interface, not just one particular class.

If new methods can be shared with other interface decorators, then we can formalize the extension with a new extended interface:
public interface AnExtendedInterface extends AnInterface {

  /** Extra convenience method.  */
  public int convenienceMethod();
}

Only the declaration of the interface decorator needs to change. The implementation remains the same.
public class InterfaceDecorator implements AnExtendedInterface {
   // Same as before...
}

You can also use interface decorators to avoid partially abstract base classes, which have only some methods implemented. You can extend only one abstract base without multiple inheritance, which C++ guides discourage, and which Java forbids. On the other hand, an interface decorator can implement as many interfaces as it wants.

I still use abstract classes sparingly, but always for a shared implementation of an interface decorator. These abstract classes only help me avoid typing the same wrappers and delegators over again, when only a few parts of the contained interface need to be changed.

Instead of abstract base classes, J. Bloch's "Effective Java (second edition)" recommends a concrete "forwarding class" (Item 16) that implements all existing methods of the interface by delegating to the contained instance of the interface. The class is a pure wrapper, implementing the same interface, and changing no behavior of the contained interface. For example,
public class InterfaceForwarder implements AnInterface {

  private final AnInterface containedInterface;

  /** Constructor wraps an instance of the same interface */
  public InterfaceForwarder(AnInterface containedInterface) {
    this.containedInterface = containedInterface;
  }

  // Delegate to contained implementation.
  public void method1() {
    containedInterface.method1();
  }

  // Delegate to contained implementation.
  public int method2(int value) {
    return containedInterface.method2();
  }

  // Delegate to contained implementation.
  public int method3() {
    return containedInterface.method3();
  }
}

An interface decorator then extends this forwarding class and overrides only the methods than need new behavior. (This results in one additional delegation for some methods.)
public class InterfaceDecorator extends InterfaceForwarder {

  /** Constructor wraps an instance of the same interface */
  public InterfaceForwarder(AnInterface containedInterface) {
    super(containedInterface);
  }

  // method1 is unchanged and inherited from InterfaceFowarder

  // Modify implementation.
  public int method2(int value) {
    value += 1;
    return super.method2(value) + 1;
  }

  // Completely change implementation.
  @Override public int method3() {
    return 5;  // ignoring contained implementation
  }

  /** Convenience method using contained implementations. */
  public int convenienceMethod() {
    method1();
    return method2(7);
  }
}

As with an abstract class, however, you are still obliged to extend exactly one class.

There is another special case worth mentioning. Sometimes a class is little more than a C struct, a grouping of public data and lightweight methods. An example might be a complex number containing a real and imaginary part, or a Point3D containing X, Y, and Z coordinates. The breaking of encapsulation is justified for efficient access (a dubious claim when runtime inlining is taken into account). Such classes may have accessor methods for bean serialization or reflection, but the implementation of the class is still permanently and irrevocably exposed.

You might want to extend this struct-class to add data, but then you should ask if it remains lightweight enough for its original purpose. Worse, you will find it impossible to override equals and maintain transitivity. (See Bloch, item 8.)

Instead of adding methods to the original class, you can write a utility class with static methods that take an instance of the struct-class as an argument.

Bill Harlan, December 2008


Return to parent directory.