Low-level designCore6 min
Inheritance
A subclass reuses everything its parent does and specializes what it needs. That reuse ties the subclass to how the parent works inside, so a parent meant to be extended must say exactly what a subclass may change.
A museum's gate
Gate admits visitors one at a time or in groups, and records each entry.
1 · The situation
A gate that also counts
A museum's entrance software has a Gate: it lets a visitor in with admit(), lets a school group in with admitGroup(), and records every entry. The museum now wants a running count of visitors at one entrance. A CountingGate that extends Gate gets all of that for free and adds only the counting:
class Gate {
private final List<String> entries = new ArrayList<>();
// Lets one visitor in and records the entry.
void admit(String visitor) {
entries.add(visitor);
}
// Lets a group in, one visitor at a time.
void admitGroup(List<String> group) {
for (String visitor : group) admit(visitor);
}
List<String> entries() {
return List.copyOf(entries);
}
}
class CountingGate extends Gate {
private int count;
@Override
void admit(String visitor) {
count++;
super.admit(visitor);
}
@Override
void admitGroup(List<String> group) {
count += group.size();
super.admitGroup(group);
}
int count() {
return count;
}
}Generalization: CountingGate is a kind of Gate. It inherits entries(), overrides admit() and admitGroup(), and adds count().
A subclass can | Meaning | Here |
|---|---|---|
Inherit | Use the parent's methods as they are |
|
Override | Replace a parent method with its own version, often calling the parent's with |
|
Add | Offer new fields and methods |
|
2 · The change
Four visitors, counted as seven
One visitor walks in, then a group of three. Four people, four entries recorded:
Entries recorded: 4 Visitors counted: 7
The entries are right; the count is three too high.
CountingGate.admitGroup() adds three to the count, then calls Gate.admitGroup() to do the admitting. But Gate.admitGroup() admits each visitor by calling admit(), and since the object is a CountingGate, that call runs CountingGate.admit(), which counts each of them again:
Both columns are the same object: its CountingGate code and its inherited Gate code. A call from Gate's code to admit() runs the subclass's override.
3 · Why it's fragile
The subclass depends on the parent's insides
The bug comes from a detail nobody promised: that Gate.admitGroup() happens to call admit() once per visitor. The obvious patch, to stop overriding admitGroup() and count only in admit(), works today. It breaks the day someone speeds up Gate.admitGroup() to record the whole group at once, without calling admit(), and the count drops to one for every group.
Either way, a change inside the parent, invisible to its other callers, breaks the subclass. That's the fragile base class problem: a subclass that overrides methods depends not only on what its parent does but on how, and the parent's author may not know it exists.
4 · The fix
Design the parent for extension
A parent meant to be extended should say exactly what a subclass may change, and keep everything else fixed:
admit() and admitGroup() become final (in C++, they simply aren't virtual). A subclass can no longer replace them, so their insides are free to change.
class Gate {
private final List<String> entries = new ArrayList<>();
// Lets one visitor in and records the entry.
final void admit(String visitor) {
entries.add(visitor);
onAdmitted(visitor);
}
// Lets a group in, one visitor at a time.
final void admitGroup(List<String> group) {
for (String visitor : group) admit(visitor);
}
List<String> entries() {
return List.copyOf(entries);
}
// For subclasses: runs exactly once for every visitor let in, however they came.
protected void onAdmitted(String visitor) {
}
}Entries recorded: 4 Visitors counted: 4
Four visitors, four counted. Gate can now admit groups any way it likes, as long as it keeps the hook's promise.
5 · When it fits
Inheritance is for "is a kind of"
Inheritance gives two things at once: reuse of the parent's code, and the right to stand in wherever a parent is expected. Use it when you want both:
- The subclass really is a kind of the parent, everywhere the parent is used: a counting gate is a gate.
- The parent was built to be extended, with its overridable parts stated.
- The hierarchy stays shallow. Each level inherits everything above it; two or three levels are plenty.
When you only want the parent's code, not its type, holding a parent object and calling it usually serves better. That's the page Composition over inheritance. And a subclass that refuses a parent method, by throwing when it's called, is a sign it isn't a kind of the parent at all; the page Liskov substitution is about that.
6 · Across languages
What can be overridden, by default
Language | Overridable by default? | To forbid overriding |
|---|---|---|
Java | Yes, every non-private, non-static method |
|
C++ | No: only | Leave it non-virtual, or mark an override |
JavaScript | Yes, every method | No keyword; document it |
Python | Yes, every method |
|
C++ is the odd one out: a method a subclass can replace has to be declared virtual, so in the first version admit() was virtual for the bug to happen at all. In the other languages, every method is a potential hook unless you close it, so the choice of what to leave open is yours to make on purpose.
7 · In practice
Vehicles at the garage
In the parking-lot problem, cars, motorbikes and trucks are kinds of vehicle, and a level should accept any of them. The checks expect each to extend Vehicle, and flag a hierarchy four or more levels deep.
Design the parking lot and keep its hierarchy shallow.
Check yourself
4 questions
Takeaways
Remember this
- A subclass inherits its parent's members, overrides some, and adds its own.
- A call from the parent's code to an overridable method runs the subclass's version.
- The fragile base class problem: subclasses that override depend on how the parent works inside.
- Design a class for extension, with stated hooks and fixed everything else, or forbid extending it.
- Use inheritance for "is a kind of"; to reuse code without the type, hold an object instead.