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.
Shops and their hours
A banner says whether a shop is open; a planner says when it next opens.
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:
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;
}
}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.
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.
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;
}
}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.
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 |
| Callers learn the storage format and repeat the logic |
Right |
| None: the questions callers ask, in their terms |
Too high |
| 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
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.