Are default method implementations bad?
Surely I am not the only one who feels that, except for some very rare occasions, a method should either be abstract or final. Let me explain why.
When analysing a piece of code (e.g. to extend or to debug it), I am often fooled by an innocent looking method like this one (taken out of Martin Fowler’s excellent book on Refactoring, bottom of page 49):
class Price {
....
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
When executing code like this, I may be surprised to find that the method is returning “2” instead of “1”. After wasting time adding logging or invoking the debugger, I eventually find that I am actually dealing with a subclass that overrides the method with:
class NewReleasePrice extends Price {
....
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
}
Am I the only one who falls for this time and time again? Am I over-engineering if I instead use a strategy pattern? E.g.:
class Price {
....
private FrequentRenterStrategy frequentRenterStrategy;
Price(FrequentRenterStrategy frequentRenterStrategy) {
this.frequentRenterStrategy = frequentRenterStrategy;
}
final int getFrequentRenterPoints(int daysRented) {
return frequentRenterStrategy.getFrequentRenterPoints(daysRented);
}
}
interface FrequentRenterStrategy {
int getFrequentRenterPoints(int daysRented);
}
final class DefaultFrequentRenterStrategy implements FrequentRenterStrategy
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
final class NewReleaseFrequentRenterStrategy implements FrequentRenterStrategy
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
}
class RegularPrice extends Price {
RegularPrice() {
super(new DefaultFrequentRenterStrategy ());
}
....
}
Admittedly, this results in more classes and more code to write, but I find that I am far less likely to misunderstand what is going on. Am I alone in feeling this way?
About the only time I am happy to see a default implementation of a method is in a Decorator framework (see “Design Patterns” by Gamma, Helm, Johnson, and Vlissides). If a number of decorators are to be developed, then it is convenient to have an abstract decorator that delegates everything to the object it is decorating. This is clearly designed to have its methods over-ridden as a convenience to the creators of concrete decorators that only need to over-ride a small subset of the methods.
I might have had a different opinion if methods in Java were final by default and, as with C++, you had to explicitly state when you want a method to be virtual. If the method had the keyword “virtual” out the front then I might be more likely to expect it to be over-ridden by subclasses. PLEASE, NO COMMENTS ABOUT HOW C# IS BETTER THAN JAVA WITH RESPECT TO THIS :-)
When analysing a piece of code (e.g. to extend or to debug it), I am often fooled by an innocent looking method like this one (taken out of Martin Fowler’s excellent book on Refactoring, bottom of page 49):
class Price {
....
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
When executing code like this, I may be surprised to find that the method is returning “2” instead of “1”. After wasting time adding logging or invoking the debugger, I eventually find that I am actually dealing with a subclass that overrides the method with:
class NewReleasePrice extends Price {
....
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
}
Am I the only one who falls for this time and time again? Am I over-engineering if I instead use a strategy pattern? E.g.:
class Price {
....
private FrequentRenterStrategy frequentRenterStrategy;
Price(FrequentRenterStrategy frequentRenterStrategy) {
this.frequentRenterStrategy = frequentRenterStrategy;
}
final int getFrequentRenterPoints(int daysRented) {
return frequentRenterStrategy.getFrequentRenterPoints(daysRented);
}
}
interface FrequentRenterStrategy {
int getFrequentRenterPoints(int daysRented);
}
final class DefaultFrequentRenterStrategy implements FrequentRenterStrategy
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
final class NewReleaseFrequentRenterStrategy implements FrequentRenterStrategy
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
}
class RegularPrice extends Price {
RegularPrice() {
super(new DefaultFrequentRenterStrategy ());
}
....
}
Admittedly, this results in more classes and more code to write, but I find that I am far less likely to misunderstand what is going on. Am I alone in feeling this way?
About the only time I am happy to see a default implementation of a method is in a Decorator framework (see “Design Patterns” by Gamma, Helm, Johnson, and Vlissides). If a number of decorators are to be developed, then it is convenient to have an abstract decorator that delegates everything to the object it is decorating. This is clearly designed to have its methods over-ridden as a convenience to the creators of concrete decorators that only need to over-ride a small subset of the methods.
I might have had a different opinion if methods in Java were final by default and, as with C++, you had to explicitly state when you want a method to be virtual. If the method had the keyword “virtual” out the front then I might be more likely to expect it to be over-ridden by subclasses. PLEASE, NO COMMENTS ABOUT HOW C# IS BETTER THAN JAVA WITH RESPECT TO THIS :-)
1 Comments:
Yes, as I indicate in my follow-up entry, I have changed my mind. For many simple cases, implementation inheritance is simpler and hence preferable to composition. I still think it is a pity that, in Java, methods are virtual by default instead of final. When analysing code, I find it really helps when a comment is used to highlight when a method is simply a default implementation expected to be over-ridden by some sub-classes.
Post a Comment
<< Home