Abstraction
An abstraction is a deliberately partial model: it keeps the handful of details one context cares about and throws the rest away on purpose. Not because the rest doesn't exist - because it doesn't matter here.
What breaks without it
Without abstraction, every class tends toward modeling the entire real-world thing it
represents, because there is no principled reason to leave anything out. An Airplane
class with no abstraction boundary ends up owning speed, altitude, rollAngle,
seatMap, fuelLoad, boardingGroup and maintenanceLog all at once, because a booking
feature needed the seat map and a simulator feature needed the roll angle and nobody ever
said no. Every one of those fields becomes a dependency, so a change meant for the
maintenance team can now break the checkout flow that never touched maintenance code -
it just happened to sit in the same class. Abstraction is what keeps unrelated concerns
from becoming accidentally coupled through one bloated model.
Worked example: one airplane, two contexts
A flight simulator's Airplane tracks speed, altitude and three rotation angles,
because physics is the whole job. A booking system's Airplane tracks a seat map, because
selling seats is the whole job. Same real object, two honest models, zero overlap.
The same trick shows up outside travel apps. A Photo uploaded to a social feed models
caption, likeCount and a handful of filters. The same photo, opened in a print shop's
ordering tool, models dpi, colorProfile and paperSize.
Neither model is wrong, and neither one needs the other's fields. Modeling "everything true about a photo" in one class would force the feed's rendering code to sit next to the print shop's color-management code forever, coupled by nothing but the accident of describing the same physical object.
Worked example: an interface as pure abstraction
Abstraction doesn't stop at "which fields does a class have" - a whole type can be an
abstraction. An Airport that accepts anything implementing FlyingTransport will happily
handle an airplane, a helicopter, or a domesticated gryphon, because it only ever knew the
method signature, never the concrete class behind it.
Abstract classes vs interfaces
Both let you program against a name instead of a concrete type, but they answer different questions and cost different things:
| Interface | Abstract class | |
|---|---|---|
| Holds fields | no (constants only) | yes |
| Holds implemented methods | in most modern languages, only ones marked default/virtual | yes, freely |
| A class can have how many | as many as it needs | exactly one |
| What it models | "this class can do X" - a capability, no shared code implied | "this class is a kind of Y" - a shared identity with real, inherited logic |
| Use it when | unrelated classes share a contract but nothing else - Helicopter, Airplane and Gryphon share no code at all | related classes share both a contract and actual implementation worth inheriting once |
FlyingTransport above is an interface for exactly this reason: a helicopter's rotor
physics and an airplane's wing physics have nothing to reuse from each other. If instead
every flying vehicle shared a real, non-trivial preFlightChecklist() implementation, an
abstract AbstractFlyingTransport class would be the better fit, because there would be
actual code worth inheriting, not just a signature worth promising.
What makes an abstraction leaky
A Photo.prepareForPrint() method that still forces the caller to check
if (photo.sourceFormat === 'RAW') convertColorSpace(...) before it can proceed is a
leaky abstraction - one where using the interface correctly requires knowing what's
behind it. The interface promised "you don't need to know the internals," and the promise
was broken the moment the caller had to branch on an internal detail to use it safely.
The same leak shows up in a Cache meant to hide which cache you're using: if
cache.get(key) is declared to throw a RedisConnectionException, every caller now has to
import a Redis-specific exception type and decide what to do with it, even though the whole
point of Cache was to make the storage swappable later. The abstraction bought a new name
for Redis, not a new model that works the same regardless of what's behind it.
Common mistakes:
- Building a leaky abstraction, as above - checking internal details through a supposedly opaque interface, or letting implementation-specific types escape through a generic signature.
- Premature abstraction - writing an interface for a single implementation "in case we need a second one someday." An interface is a bet about what will vary; a bet placed before you have a second real implementation to compare against is usually placed in the wrong spot, and gets reshaped the day the second implementation actually shows up.
Where this shows up in the patterns catalogue
- Bridge splits an abstraction from its implementation as its literal job description - the pattern exists specifically so the two can vary independently, which is abstraction's whole promise made structural.
- Template Method puts an abstract skeleton in a base class and asks subclasses to fill in the steps - the caller only ever addresses the abstract method names, never the concrete fill-in.
- Factory Method returns an abstract product type from its creation method, so callers depend on the abstraction and never learn which concrete class they received.
Try it yourself: design a Logger abstraction that has to work for both a CLI tool
printing to stdout and a production service shipping structured JSON to a log aggregator -
what single method earns a place on the interface, and what do you deliberately leave out?
Then check your answer against the leak test above: could a caller use your interface
correctly without knowing which implementation is behind it?
Check yourself
A flight simulator and a booking site both have an Airplane class, and they share almost no fields. Which pillar explains that?