Why Extends is Evil
import java.util.ArrayList;
public class SimpleSentence
{
private ArrayList<Character> letters = new
ArrayList<Character>();
public void add(char symbol)
{
letters.add(symbol);
}
public void addWord(char[] word)
{
for (int i=0; i <
word.length; i++)
{
add(word[i]);
}
}
public void remove()
{
letters.remove(0);
}
public String say()
{
String result = "";
for (Character ch: letters)
{
result += ch;
}
return result;
}
}
Say we need a new kind of sentence that has only alphabetic
characters,
an AlphaSentence. It has many of the same characteristics, so
let's extend
Simple Sentence and override the add() method.
/**
* AlphaSentence is a SimpleSentence but comprised of alphabetic
characters.
* It also has a size.
*/
public class AlphaSentence extends SimpleSentence
{
private int size = 0;
public void add(char symbol)
{
if
(Character.isLetter(symbol)) // allow only Alphabetic characters
{
size++;
super.add(symbol);
}
}
public int getSize()
{
return size;
}
}
This will compile and run, and the first two test cases pass.
When addWord() is called on AlphaSentence, Java
invokes the parent's addWord() method, which then
calls add(). Happily, Java is smart enough
to know that the call to add() in SimpleSentence
should really be to add() in AlphaSentence.
public void testOne()
{
AlphaSentence alphaSen1 = new
AlphaSentence();
alphaSen1.add('D');
alphaSen1.add('O');
alphaSen1.add('3');
alphaSen1.add('G');
assertEquals(3,
alphaSen1.getSize());
}
public void testWord()
{
char[] dog = {'D','O','3','G'};
AlphaSentence alphaSen1 = new
AlphaSentence();
alphaSen1.addWord(dog);
assertEquals(3,
alphaSen1.getSize());
}
Unfortunately testRemove() fails. Can you tell why?
public void testRemove()
{
char[] dog = {'D','O','G'};
AlphaSentence alphaSen1 = new
AlphaSentence();
alphaSen1.addWord(dog);
assertEquals(3,
alphaSen1.getSize());
alphaSen1.remove();
assertEquals("OG",alphaSen1.say());
assertEquals(2, alphaSen1.getSize());
}
???
The solution is that we have to override remove() also.
public void remove()
{
size--;
super.remove();
}
So inheritance bought us a little savings because we don't
have to rewrite addWord().
But we have to override remove() and any other
methods that manipulate size.
So implementation-inheritance isn't always all it's cracked up to be.
However, a deeper problem still exists. Let's say that we
discover a better implementation
of SimpleSentence.
/* The improved implementation uses StringBuffer instead of
ArrayList */
private StringBuffer letters = new StringBuffer();
public void add(char symbol) {
letters.append(symbol); }
public void addWord(char[] word) {
letters.append(word); }
public void remove() {letters.deleteCharAt(0); }
public String say() { return letters.toString();
}
Now AlphaSentence still compiles and runs, but testWord()
fails.
Can you explain why?
This is a big problem. Changing the implementation of a parent
class
might break its children. So we have to retest all the children,
and possibly
change their implementations. This is another aspect of the
Fragile-Base Class problem.
And that's why Extends is Evil.
When would this happen? A common situation is when a developer writes a class, and another developer on the same team writes a subclass, trying to simplify their job by relying on a parent class implementation (visible in the team repository). Then later a third developer changes the parent class implementation,
unaware that a child class is reusing that implementation.
A more flexible and robust design approach is to use
interface-inheritance and composition.
Can you use this approach to fix this design?
???
public interface Sayable
{
public void add(char symbol);
public void addWord(char[] word);
public void remove();
public String say();
}
import java.util.ArrayList;
public class SimpleSentence implements Sayable
{
private ArrayList<Character> letters = new
ArrayList<Character>();
/* The remainder of the class is the same as the original */
}
/**
* AlphaSentence is a sentence of alphabetic characters.
*/
public final class AlphaSentence implements Sayable
{
private int size = 0;
private SimpleSentence words = new SimpleSentence();
public void add(char symbol)
{
if
(Character.isLetter(symbol)) // allow only Alphabetic characters
{
size++;
words.add(symbol);
}
}
public void addWord(char[] word)
{
for (int i=0; i < word.length; i++)
{
add(word[i]);
}
}
public void remove()
{
size--;
words.remove();
}
public int getSize()
{
return size;
}
public String say()
{
return words.say();
}
}
Now if the implementation of SimpleSentence changes, it has
no effect on its clients.