Inheritance as Polymorphism vs Inheritance as Code Sharing
We've all heard the mantra that we should favor composition over inheritance, and in most cases this is good advice. Occasionally, though, we really do need an inheritance relationship. Inheritance provides that useful “is-a” relationship that sometimes fits a situation so well. The pitfall we have to watch out for is using inheritance for code sharing between classes that should otherwise not be related, and not for polymorphism, that “is-a” relationship.
What makes code sharing through inheritance so bad in the first place? An inheritance relationship is one of the tightest dependencies that could be made between two classes. It is hard-compiled in, and cannot be swapped out at runtime. It's an all-or-nothing relationship, and anything the parent wants to expose, the child must also expose. The child class may be able to override some of the parent behaviors, but the parent may finalize some methods and prevent this, locking the child class in even more tightly. This may be beneficial and desired in the right case, but if an inheritance relationship is shoe-horned in to share code, this can terribly distort the code base.
So sharing code through inheritance is bad, but how can we differentiate between inheritance as polymorphism versus inheritance as code sharing. There is a simple smell you can use to tell the difference. If you are driving the instance through the interface of the superclass, it is generally polymorphism; if you are driving the instance through the interface of the subclass, it is generally code sharing. Now this is a heuristic, a code smell, not a hard-and-fast rule, but it should at least point you in the right direction.
A few examples, starting with a case of polymorphism:
The code above uses the subclasses according to the interface of the superclass (an example of the strategy pattern). This is pretty easy to see here since the subclasses don't have any additional behavior. It's even more obvious because in the
Now an example of code sharing:
Here the fact that we are just sharing code is pretty obvious for many reasons. The use of
By breaking inheritance as code sharing relationships by using other tools such as composition and extracting classes to achieve single responsibility, we can get code that is more flexible and much easier to maintain.
What makes code sharing through inheritance so bad in the first place? An inheritance relationship is one of the tightest dependencies that could be made between two classes. It is hard-compiled in, and cannot be swapped out at runtime. It's an all-or-nothing relationship, and anything the parent wants to expose, the child must also expose. The child class may be able to override some of the parent behaviors, but the parent may finalize some methods and prevent this, locking the child class in even more tightly. This may be beneficial and desired in the right case, but if an inheritance relationship is shoe-horned in to share code, this can terribly distort the code base.
So sharing code through inheritance is bad, but how can we differentiate between inheritance as polymorphism versus inheritance as code sharing. There is a simple smell you can use to tell the difference. If you are driving the instance through the interface of the superclass, it is generally polymorphism; if you are driving the instance through the interface of the subclass, it is generally code sharing. Now this is a heuristic, a code smell, not a hard-and-fast rule, but it should at least point you in the right direction.
A few examples, starting with a case of polymorphism:
public abstract class SalesTaxCalculator {
public abstract double getTaxOn(double price);
public final double getTotalPriceOn(final double price) {
return price + getTaxOn(price);
}
}
public class DelawareSalesTaxCalculator extends SalesTaxCalculator {
@Override public double getTaxOn(double price) {
return 0.0;
}
}
public class CaliforniaSalesTaxCalculator extends SalesTaxCalculator {
@Override public double getTaxOn(double price) {
return price * 1.075;
}
}
public class Main {
public static main(String[] args) {
printPriceAndTaxes(10.0, new DelawareSalesTaxCalculator());
printPriceAndTaxes(10.0, new CaliforniaSalesTaxCalculator());
}
private static void printPriceAndTaxes(double price, SalesTaxCalculator taxCalculator) {
System.out.printf("Price: %.2f%n", price);
System.out.printf("Tax: %.2f%n", taxCalculator.getTaxOn(price));
System.out.printf("Total: %.2f%n", taxCalculator.getTotalPriceOn(price));
}
}
The code above uses the subclasses according to the interface of the superclass (an example of the strategy pattern). This is pretty easy to see here since the subclasses don't have any additional behavior. It's even more obvious because in the
printPriceAndTaxes
method, the taxCalculator
is typed to SalesTaxCalculator
, though this is not strictly necessary.Now an example of code sharing:
public abstract class Bird {
public String fly() {
return "Flap flap flutter";
}
public abstract String tweet();
}
public class Parrot extends Bird {
public boolean caged = false;
public Parrot(boolean caged) {
this.caged = caged;
}
@Override public String fly() {
if (caged) {
throw new CannotFlyException("A caged parrot cannot fly away...");
}
return super.fly();
}
@Override public String tweet() {
return talk("Polly wanna cracker!");
}
public String talk(String say) {
return "Squawk! " + say;
}
}
public class Penguin extends Bird {
@Override public String fly() {
throw new CannotFlyException("A penguin cannot fly away...");
}
public String swim() {
return "Splash!";
}
@Override public String tweet() {
return "Ahh!";
}
}
public class Main {
public static main(String[] args) {
Collection birds = Arrays.asList(
new Parrot(false), new Parrot(true), new Penguin());
for(Bird bird : birds) {
try {
System.out.println(bird.fly());
} catch(CannotFlyException ex) {
if(bird instanceof Penguin) {
System.out.println(((Penguin)bird).swim());
} else {
System.out.println(ex.getMessage());
}
}
System.out.println(bird.tweet());
if(bird instanceof Parrot) {
System.out.println(((Parrot)bird).talk("Polly want a cookie"));
}
}
}
}
Here the fact that we are just sharing code is pretty obvious for many reasons. The use of
instanceof
tells us that we are more interested in the subclasses than we are of the superclass, even though we are storing each as an instance of the superclass. We can also see this by the fact that we are using only some of the behavior of the superclass. Parrot
uses the parent fly
method conditionally, and Penguin
does not use it at all. The shared code is the fly
method (partially), and the type that allows these disparate instances to be grouped together.By breaking inheritance as code sharing relationships by using other tools such as composition and extracting classes to achieve single responsibility, we can get code that is more flexible and much easier to maintain.
Comments
Post a Comment