Low-level designCore7 min

Abstraction

Offer what callers need to know, in their own terms, and keep how it's done to yourself. Then the inside can change, and callers never notice.

Watch it evolve1 / 8
The problem

Shops and their hours

A banner says whether a shop is open; a planner says when it next opens.

80
Start
Stage 1
Final

1 · The situation

Opening hours, as the shop types them

A neighborhood app lists local shops. Each shop's hours are typed in as ranges such as 08:00-12:00 and 13:00-18:00, and two parts of the app use them: a Banner that says whether a shop is open right now, and a Planner that says when it next opens.

OpeningHours keeps the ranges private and hands out a read-only list, so nobody can change a shop's hours by accident. Each caller reads the ranges and works out what it needs:

Callers read the ranges and parse themJava
class OpeningHours {
    private final List<String> ranges;

    // Each range is "HH:MM-HH:MM", for example "08:00-12:00".
    OpeningHours(String... ranges) {
        this.ranges = List.of(ranges);
    }

    List<String> ranges() {
        return ranges;
    }
}

class Banner {
    String text(OpeningHours hours, int minute) {
        for (String r : hours.ranges()) {
            int start = Clock.minutes(r.substring(0, 5));
            int end = Clock.minutes(r.substring(6));
            if (start <= minute && minute < end) return "open";
        }
        return "closed";
    }
}

class Planner {
    // The next time a range starts, today or else tomorrow.
    int nextOpening(OpeningHours hours, int minute) {
        int next = -1;
        int first = -1;
        for (String r : hours.ranges()) {
            int start = Clock.minutes(r.substring(0, 5));
            if (first < 0 || start < first) first = start;
            if (start > minute && (next < 0 || start < next)) next = start;
        }
        return next >= 0 ? next : first;
    }
}
OpeningHours− ranges: List<String>~ ranges(): List<String>Banner~ text(OpeningHours, int): StringPlanner~ nextOpening(OpeningHours, int): intClock~ minutes(String): int~ text(int): String

Drawn from the code: Banner and Planner both use OpeningHours, and both use the time helpers to parse what it hands them.

2 · The change

A bar that's open past midnight

A bar joins the app, open from 18:00-02:00. Ask about it at one in the morning:

Bakery at 12:30: closed, opens at 13:00
Bar at 01:00: closed, opens at 18:00
Bar at 03:00: closed, opens at 18:00

The bar is open at 01:00, but the banner says it's closed.

The banner checks start <= minute < end, which assumes a range ends later in the day than it starts. A range across midnight breaks that, and the fix must go into every caller that parses the ranges: the banner now, the planner and anything else later. Each one copied how the hours are stored and how to read them.

Callers read the strings
2
Callers ask questions
0

Classes outside OpeningHours that change when the way hours are stored or read changes.

3 · Encapsulated, yet exposed

Private state can still leak its shape

Notice what didn't go wrong. Nobody changed the bar's hours; OpeningHours is properly encapsulated, and its list is read-only. The problem is that its one method, ranges(), hands out the hours in the form they're stored, so every caller depends on that form: two-digit times, a dash in the middle, a range that ends after it starts.

Encapsulation decides who may change the state. Abstraction decides what callers get to know about it. A class can do the first well and the second badly, as this one does.

4 · The fix

Answer the callers' questions instead

Look at what the callers actually want to know: is it open at this time, and when does it next open? Make those the class's methods, and keep the ranges to itself:

isOpenAt(minute) and nextOpening(minute) replace ranges(). They're named for what callers need, in the callers' terms, and say nothing about strings or dashes.

Java
class OpeningHours {
    // Each range as [start, end] in minutes after midnight; end < start runs past midnight.
    private final List<int[]> ranges = new ArrayList<>();

    // Each range is "HH:MM-HH:MM", for example "08:00-12:00" or "18:00-02:00".
    OpeningHours(String... ranges) {
        for (String r : ranges) {
            int start = Clock.minutes(r.substring(0, 5));
            this.ranges.add(new int[] {start, Clock.minutes(r.substring(6))});
        }
    }

    boolean isOpenAt(int minute) {
        for (int[] r : ranges) {
            boolean open = r[0] < r[1]
                    ? r[0] <= minute && minute < r[1]
                    : minute >= r[0] || minute < r[1];
            if (open) return true;
        }
        return false;
    }

    // The next time a range starts, today or else tomorrow.
    int nextOpening(int minute) {
        int next = -1;
        int first = -1;
        for (int[] r : ranges) {
            if (first < 0 || r[0] < first) first = r[0];
            if (r[0] > minute && (next < 0 || r[0] < next)) next = r[0];
        }
        return next >= 0 ? next : first;
    }
}
OpeningHours− ranges: List<int[]>~ isOpenAt(int): boolean~ nextOpening(int): intBanner~ text(OpeningHours, int): StringPlanner~ nextOpening(OpeningHours, int): intClock~ minutes(String): int~ text(int): String
Bakery at 12:30: closed, opens at 13:00
Bar at 01:00: open
Bar at 03:00: closed, opens at 18:00

The bar is open at 01:00, and the bakery's lunch break still works.

5 · What it is

What, not how

Abstraction is choosing what a class shows: the operations that matter to its callers, described by what they mean rather than how they work. The rest, the data structure and the algorithm, stays inside, free to change. The three words below are often used interchangeably, but each names something different:

Idea

The question

Here

Abstraction

What should callers see and depend on?

"Is it open at 01:00?", not a list of strings

Encapsulation

Who may change the state, and through what?

The ranges are private and read-only

Information hiding

Which decisions are likely to change, and are they hidden?

How hours are stored and read

Information hiding is the reason, abstraction is the interface it leads to, and encapsulation is the mechanism that enforces it. The first version had the mechanism without the interface.

OpeningHours− ranges: List<int[]>~ isOpenAt(int): boolean~ nextOpening(int): intBanner~ text(OpeningHours, int): StringPlanner~ nextOpening(OpeningHours, int): intClock~ minutes(String): int~ text(int): String

After the fix: OpeningHours shows two questions, and its representation is private. Banner and Planner no longer need the time helpers.

6 · Choosing the level

Not too low, not too high

Level

Example

The problem

Too low

ranges() returning "18:00-02:00"

Callers learn the storage format and repeat the logic

Right

isOpenAt(minute), nextOpening(minute)

None: the questions callers ask, in their terms

Too high

bannerText() on OpeningHours

Takes on the banner's wording, which changes for other reasons

A good test: describe each method without mentioning how the class works inside. "Is the shop open at this minute?" passes. "Returns the list of range strings" fails, and so does any method whose callers must know a format, an index or an order to use it.

7 · Leaks

Signs the how is getting out

An abstraction leaks when callers have to know something about the inside to use it correctly. Common signs:

  • Callers parse or format what they get back, as the banner did with the range strings.
  • Names describe storage: getRangeList(), rowsForShop().
  • Errors from the inside escape, such as a database error from a method that was meant to answer "is it open?".
  • Flags that steer the inside: isOpenAt(minute, useNewParser).

Some leaking can't be avoided; how slow a call is, for example, may matter to callers. The aim is to keep what callers must know small and deliberate. Interfaces and abstract classes, on later pages, are tools for the same idea: they let callers depend on what something does while several classes provide the how.

8 · In practice

Ask the level for a spot

In the parking-lot problem, a lot that reads each level's list of spots and checks sizes itself depends on how levels store spots. A level that answers "find me a spot for this vehicle" lets the lot ask the question and keeps the how inside.

Design the parking lot and choose what each class shows.

Check yourself

4 questions

1. Why did the first version say the bar was closed at 01:00?
2. OpeningHours had private fields and a read-only list. What was wrong with it?
3. Which method is the best abstraction for the banner's needs?
4. Which is a sign of a leaky abstraction?

Takeaways

Remember this

  • Abstraction is choosing what callers see: the operations they need, in their terms, not how the class works.
  • Encapsulation decides who may change the state; abstraction decides what callers may know about it.
  • Shape methods around callers' questions, such as isOpenAt(), and the representation can change freely.
  • Too low exposes storage; too high takes on a caller's job.
  • An abstraction leaks when callers must parse, index or otherwise know the inside to use it.