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.

Watch it evolve1 / 8
The problem

A diary of workouts and meals

Runs, rides and meals all implement one interface, Activity.

80
Start
Stage 1
Final

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:

One interface for every entry, and a meal that doesn't fit itJava
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) {
    }
}
*«interface»Activity+ name(): String+ minutes(): int+ netCalories(): int+ addLap(int)Run− minutes: int− laps: List<Integer>+ name(): String+ minutes(): int+ netCalories(): int+ addLap(int)~ lapCount(): intRide− minutes: int− laps: List<Integer>+ name(): String+ minutes(): int+ netCalories(): int+ addLap(int)Meal− name: String− kcal: int+ name(): String+ minutes(): int+ netCalories(): int+ addLap(int)Diary− entries: List<Activity>~ add(Activity)~ report(): List<String>

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 Activity demands minutes() and addLap(). Meal answers 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.
The diary works around the misfitJava
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.

Java
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;
    }
}
*«interface»DiaryEntry+ summary(): String+ netCalories(): int«abstract»Workout− minutes: int− laps: List<Integer>~ addLap(int)~ lapCount(): int+ summary(): String+ netCalories(): int# name(): String# kcalPerMinute(): intRun# name(): String# kcalPerMinute(): intRide# name(): String# kcalPerMinute(): intMeal− name: String− kcal: int+ summary(): String+ netCalories(): intDiary− entries: List<DiaryEntry>~ add(DiaryEntry)~ report(): List<String>
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

*«interface»DiaryEntry+ summary(): String+ netCalories(): int«abstract»Workout− minutes: int− laps: List<Integer>~ addLap(int)~ lapCount(): int+ summary(): String+ netCalories(): int# name(): String# kcalPerMinute(): intRun# name(): String# kcalPerMinute(): intRide# name(): String# kcalPerMinute(): intMeal− name: String− kcal: int+ summary(): String+ netCalories(): intDiary− entries: List<DiaryEntry>~ add(DiaryEntry)~ report(): List<String>

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

interface; a class implements many

abstract class; a class extends one

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 new on itself

Python

typing.Protocol: matched by shape, checked by type checkers

abc.ABC with @abstractmethod; can't be created until all are defined

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

1. A meal must implement addLap() and leaves it empty. What does that say about the interface?
2. Runs and rides share fields and a calories formula. Where should that code live?
3. Which can a Java class have several of?
4. What should the diary depend on?

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.