Lazy Inheritance
If you've done any object oriented development, you've probably heard the terms is-a and has-a. Is-a is a way to determine if one object should inherit from another. It's not the best criteria to use, but it is a pretty good rule of thumb. Unfortunately, some developers get into the habit of inheriting from another class just to get some reuse. Some method in a class does some work that they need, so they extend that class to get that work for free. That's lazy inheritance.
Lazy Inheritance Bugs
When you use lazy inheritance, you begin a cascading hierarchy that can result in bugs. You extend some class to get some free work. Someone else extends your class to get some of your free work. Someone extends that class to get more free work until you have some methods very far down the hierarchy that depend on methods way up the hierarchy. Now, the author of the original class changes a method and something completely different breaks. That's the Lazy Inheritance Bug.
And that's why terms like is-a exist. To give you some criteria from which to decide if you should inherit from another class. Barbara Liskov provided a better criteria called the Liskov Substitution Principle (LSP). Basically, given a class A that inherits from class B then any class C that has a reference to class A should also work when given a reference to class B without having to know it has a B. In other words, class B should be substitutable for class A. When you use lazy inheritance, you violate the LSP by definition.
Avoiding lazy inheritance bugs is easy: don't use inheritance unless you can conform to the LSP.
Delegation
Before you use inheritance to get reuse or avoid duplication, take some time to consider delegation instead. Here's an example:
Inheritance:
public class Car {
public void go() {
// goes pretty fast
}
}
public class SportsCar extends Car {
public void go() {
// goes really fast
}
}
Notice the only difference between a Car and a SportsCar is the engine. We could use delegation and only have one type of car.
public interface Engine {
void go();
}
public class EconomyEngine implements Engine {
public void go() {
// goes pretty fast
}
}
public class SportsEngine implements Engine {
public void go() {
// goes really fast
}
}
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void go() {
engine.go();
}
}
So now you can configure your car with whatever engine you want. This example doesn't seem to hold its weight. There's still inheritance going on, but now it's with Engine and not Car. What gives? Well, just add one more option to see how delegation almost always wins:
public class FamilyCar {
public void go() {
// goes pretty fast
}
public void turn() {
// turns OK
}
}
public class SportsCar {
public void go() {
// goes really fast
}
public void turn() {
// turns really well
}
}
public class SportSuv {
public void go() {
// goes really fast
}
public void turn() {
// turns OK
}
}
public class EconomyCar {
public void go() {
// goes pretty fast
}
public void turn() {
// turns really well
}
}
How can we use inheritance to combine just these two features? With delegation it's easy:
public interface Engine {
void go();
}
public class SportsEngine implements Engine {
public void go() {
// goes really fast
}
}
public class EconomyEngine implements Engine {
public void go() {
// goes pretty fast
}
}
public interface Suspension {
void turn();
}
public class SportsSuspension implements Suspension {
public void turn() {
// turns really well
}
}
public class EconomySuspension implements Suspension {
public void turn() {
// turns OK
}
}
public class Car {
private Engine engine;
private Suspension suspension;
public Car(Engine engine, Suspension suspension) {
this.engine = engine;
this.suspension = suspension;
}
pubic void go() {
engine.go();
}
public void turn() {
suspension.turn();
}
}
Now we can configure any Car with any type of Engine and Suspension. When you start thinking about the braking system and traction control and air bags and body style and on and on, you can see that trying to use inheritance for each possible car configuration would get out of hand really fast. Delegation allows you to configure your car with each component and have the reuse in the components and not in the hierarchy. And that's how to squash lazy inheritance bugs before they hatch.
5 comments:
oh noes!! it's a car analogy... ;-)
Just wondering... how much is inheritance really used? Properly, that is. The more I learn about this stuff, the more I think that the criteria for when to use inheritance are really pretty small. Even in the classic intro-to-OO Shapes example, inheritance breaks down pretty easily. It seems like inheritance for classes is often more trouble than it's worth -- maybe creates more problems than it solves. For interfaces, it's really nice, but maybe it breaks down there too?
I've not worked with a multi-inheritance language so maybe it plays better there. But in java, at least, it seems like you really need a *good* case for inheritance before you do it.
more .02
Great post. I hadn't heard of the LSP by name, but I'd heard it described once and it seems like a good guideline.
What kind of bugs did you run into that this caused?
Andrew, you are right--it should be used very rarely, but when it is needed, it is extremely helpful.
I'd guess that in a good design of a mid to largish project, fewer than 50% of your classes would contain the extends keyword, but then you get types like java's swing controls where it makes decent sense for them to all inherit from the same class.
Now that I think about it, having each control contain an object that had all the functionality in "Component" (and having them each implement a "getComponentProperties" interface to get it) might have been even better... maybe.
@Bill: yeah, LSP is great. If you are using a subclass and can't seamlessly substitute it for it's parent class, then something's wrong. It certainly helps as a tool for examining your code and looking for problems. At least, that is, in my limited experience, this has been true.
In our class (taught by Curtiss) project, there were several times where we found we had
public class Foo extends Bar
or
public interface Baz extends Bam
and were referencing either Foo or Baz in our code. When it was refactor time, we'd think about LSP, and wander through the code replacing all the Foos and Bazzes with Bars and Bams. This forced us to make the code better. At least it was helpful for us.
Hi Bill,
The bug I ran into was from a huge hierarchy of objects that all extended from JPanel. Someone had noticed that all the getToolTip() methods looked about the same and refactored them into a base class way up the chain. One of the getToolTip() methods actually needed to be special since it provided some details that the others did not.
The problem was caused by using inheritance for reuse rather than delegation. Most of the objects in the hierarchy don't even pass the is-a test let alone satisfy LSP.
Swing, awt & co. are a really horrible use of inheritance, up to 6 or 7 levels. This is a good object-oriented design, which opens many wiring possibilities and does not tie classes in a strict hierarchy. Well-written.
Post a Comment