Low-level designCore6 min
Interfaces and abstract classes
An interface is a contract: what a class promises to do, with no code or state behind it. An abstract class is a partial implementation: shared code and state for closely related classes, with gaps for each to fill. Most designs use both, each for its own job.
A diary of workouts and meals
Runs, rides and meals all implement one interface, Activity.
1 · The situation
A diary of workouts and meals
A fitness app keeps a daily diary. Runs and rides burn energy, meals add it, and the diary shows each entry and the net for the day. The first version gives everything one interface, Activity, so the diary can hold runs, rides and meals alike:
interface Activity {
String name();
int minutes();
// Eaten is positive, burned is negative.
int netCalories();
void addLap(int seconds);
}
class Meal implements Activity {
private final String name;
private final int kcal;
Meal(String name, int kcal) {
this.name = name;
this.kcal = kcal;
}
public String name() {
return name;
}
// A meal has no duration.
public int minutes() {
return 0;
}
public int netCalories() {
return kcal;
}
// A meal has no laps.
public void addLap(int seconds) {
}
}Drawn from the code: Run, Ride and Meal all implement Activity, the dashed lines with hollow triangles.
Run, 42 min: -420 kcal Ride, 60 min: -420 kcal Lunch: +650 kcal Net today: -190 kcal Laps on the run: 2
2 · The change
A contract that doesn't fit, and code written twice
The output is right, but the design is straining in three places:
- A meal has no duration and no laps, yet
Activitydemandsminutes()andaddLap().Mealanswers zero and does nothing, pretending. - The pretending leaks. The diary has to know that zero minutes means "don't show a duration".
- Runs and rides repeat each other. Both keep the minutes and the laps and compute calories the same way, differing only in a name and a rate.
class Diary {
private final List<Activity> entries = new ArrayList<>();
void add(Activity a) {
entries.add(a);
}
List<String> report() {
List<String> lines = new ArrayList<>();
int net = 0;
for (Activity a : entries) {
String when = a.minutes() > 0 ? ", " + a.minutes() + " min" : "";
lines.add(a.name() + when + ": " + String.format("%+d", a.netCalories()) + " kcal");
net += a.netCalories();
}
lines.add("Net today: " + String.format("%+d", net) + " kcal");
return lines;
}
}Two different things are tangled together: the contract the diary needs from every entry, which is small, and the shared code runs and rides could reuse, which meals don't want. The practice checks flag the misfit: Meal leaves 1 of Activity's methods empty.
3 · The fix
An interface for the contract, an abstract class for the shared code
DiaryEntry asks for only what the diary uses: a summary() and the netCalories(). A meal can honestly do both.
interface DiaryEntry {
String summary();
// Eaten is positive, burned is negative.
int netCalories();
}
class Meal implements DiaryEntry {
private final String name;
private final int kcal;
Meal(String name, int kcal) {
this.name = name;
this.kcal = kcal;
}
public String summary() {
return name;
}
public int netCalories() {
return kcal;
}
}Run, 42 min: -420 kcal Ride, 60 min: -420 kcal Lunch: +650 kcal Net today: -190 kcal Laps on the run: 2
The same diary, with no zero-minute meals and no repeated workout code.
4 · Which to use
Contract or partial implementation
Aspect | Interface | Abstract class |
|---|---|---|
It is | A contract: methods a class promises | A partial class: code and state, with gaps |
State | None (constants only, in Java) | Fields, set up by its constructor |
Code | None, or small default methods in Java | Any, shared by every subclass |
A class can have | Many: a class implements several | One: a class extends one (in Java) |
Use it for | What unrelated classes can all do | What closely related classes share |
A rule of thumb: start callers off depending on an interface, because it promises the least and is the easiest to implement in a new way. Add an abstract class behind it when several implementations turn out to share real code, as runs and rides did.
5 · Both together
The usual shape
The diary depends only on DiaryEntry. Workout implements it with shared code, Run and Ride extend Workout, and Meal implements the interface directly.
This interface-plus-abstract-class shape is common in standard libraries too: a collection interface for callers, and an abstract base class that does most of the work for the people writing new collections. Callers never see the abstract class; implementers use it if it helps and ignore it if it doesn't, as Meal does.
6 · Across languages
What each language offers
Language | Interface | Abstract class |
|---|---|---|
Java |
|
|
C++ | A class with only pure virtual functions | A class with some pure virtual functions; multiple inheritance is allowed |
JavaScript | None in the language: a documented shape | A class whose constructor refuses |
Python |
|
|
Python's Protocol matches by structure: Meal never names DiaryEntry, and a type checker accepts it because it has the right methods. That's close to how JavaScript code relies on shapes, with a checker added.
7 · In practice
Pricing and vehicles
The parking-lot problem uses both: pricing is a contract that several schemes implement, so it's an interface the lot holds; vehicles share a size and a plate, so an abstract Vehicle with subclasses fits. The checks flag classes that leave interface methods empty.
Design the parking lot and choose each one's job.
Check yourself
4 questions
Takeaways
Remember this
- An interface is a contract; an abstract class is a partial implementation with shared code and state.
- An implementer that has to leave a method empty means the interface is too wide.
- Callers depend on an interface; an abstract class behind it shares code among close relatives.
- A class can implement many interfaces but extend one class, in Java.
- Python's Protocol and JavaScript's documented shapes match by structure, not by naming the interface.