Skip to main content

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.

Airplane- speed- altitude- rollAngle- pitchAngle- yawAngle+ fly()Airplane- seats+ reserveSeat(n)flight simulatorbooking system
Different models of the same real-world object.

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.

Photo- caption- likeCount- filters+ share()Photo- dpi- colorProfile- paperSize+ prepareForPrint()social feedprint shop
Neither model needs the other's fields; the feed does not care what paper stock you print on.

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.

Airport...+ accept(v: FlyingTransport)«interface»FlyingTransport+ fly(origin, dest, people)Helicopter...+ fly(origin, dest, people)Airplane...+ fly(origin, dest, people)Gryphon...+ fly(origin, dest, people)Simple arrows indicatethat one class dependson the otherInterfaces in UML lookalmost like classes, butonly have methodsEmpty triangle headsand dashed lines meana class implementsan interface
implementsuses
Several classes implementing one interface. The airport never learns which is which.

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:

InterfaceAbstract class
Holds fieldsno (constants only)yes
Holds implemented methodsin most modern languages, only ones marked default/virtualyes, freely
A class can have how manyas many as it needsexactly 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 whenunrelated classes share a contract but nothing else - Helicopter, Airplane and Gryphon share no code at allrelated 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

Question 1 of 4

A flight simulator and a booking site both have an Airplane class, and they share almost no fields. Which pillar explains that?