Abstraction
Beginner · 10 min read · ▶ live playground · ✦ checkpoint
A reading-list feature begins with plain text. Then someone asks for comma-separated values (CSV), a table-like text format, and JavaScript Object Notation (JSON), a text format for named data. Soon the code requesting the file—the caller—contains quote handling, line joining, and a growing chain of format checks, even though it only wants a file.
An abstraction is a smaller view that exposes what a caller needs while hiding details it does not. Think of a restaurant menu: it tells you what you can order, not the burner setting or the cook's sequence of steps. Good abstraction removes decisions from the caller without erasing decisions from the design.
The visible operations form a type's public surface, the part outside code is allowed to depend on. For a reading-list export, that surface can be one action: accept book titles and return file text.
Name the “what” once
An interface is a contract that lists operations a class promises to provide. It is an abstract type, a type that names required behavior without providing a complete object you create with new. A concrete type supplies the actual method code and can be used to create objects.
The interface below says what every exporter does. The three classes decide how.
class diagram
Three formats behind one export surface
- 1ReadingListExporter exposes one essential action: turn book titles into text.
- 2PlainTextExporter implements the contract with one title per line.
- 3CsvExporter keeps commas and quote marks inside valid CSV text.
- 4JsonExporter produces JSON without changing the caller's export surface.
Each dashed hollow-triangle arrow reads “implements,” just as it did in your first UML lesson. The arrow points to the contract. Nothing in ReadingListExporter says how lines, quotes, or JSON braces are produced.
Put complexity behind the contract
An implementation is the code that fulfills a contract. These implementations make different formatting decisions while keeping the same method name, inputs, and returned type.
interface ReadingListExporter {
export(titles: string[]): string;
}
class PlainTextExporter implements ReadingListExporter {
public export(titles: string[]): string {
return titles.join("\n");
}
}
class CsvExporter implements ReadingListExporter {
public export(titles: string[]): string {
return titles
.map((title) => this.quote(title))
.join("\n");
}
private quote(value: string): string {
return `"${value.replaceAll('"', '""')}"`;
}
}
class JsonExporter implements ReadingListExporter {
public export(titles: string[]): string {
return JSON.stringify({ titles }, null, 2);
}
}
function buildFile(
exporter: ReadingListExporter,
titles: string[],
): string {
return exporter.export(titles);
}
const titles = ["Beloved", 'The "Little" Prince'];
const fileBody = buildFile(new CsvExporter(), titles);
console.log(fileBody);"Beloved" "The ""Little"" Prince"
buildFile() knows only the abstract type. It can ask for export() and trust that a string comes back. It does not know that CSV doubles an internal quote, plain text joins lines, or JSON adds a named titles field.
The caller still chooses a concrete exporter somewhere, as new CsvExporter() does here. Abstraction does not make choices disappear. It places each choice where the needed knowledge already lives.
Model only the essential features
The interface includes export() because every caller needs that operation. It does not include quoteCsvValue(), chooseIndentation(), or joinPlainTextLines(). Those are format details, and forcing them into the shared surface would make every exporter pretend to support work that does not belong to it.
Ask two questions when shaping an abstraction:
- What does the caller need to accomplish?
- Which details can change without changing that goal?
Here, “turn these titles into text” is stable. Quote escaping and brace placement vary. The interface names the stable “what”; each concrete class owns its changing “how.”
Interfaces are not the only abstractions. A well-named function can hide a calculation, and a class method can hide several state changes. An interface becomes useful when multiple concrete types need to present the same small promise to their callers.
Abstraction and encapsulation solve different problems
The ideas work together, but they are not synonyms.
- Encapsulation controls access to an object's internals so its rules remain valid.
- Abstraction limits what callers must understand so they can use a capability through a smaller surface.
CsvExporter.quote() is private, so callers cannot reach that helper directly. That is encapsulation. ReadingListExporter.export() lets buildFile() work without knowing any formatting steps. That is abstraction.
You can have one without the other. A class may hide ten private fields but still expose twenty confusing public methods, so it is encapsulated yet poorly abstracted. An interface may look wonderfully small while its implementation returns an internal array that callers can corrupt, so it is abstracted yet poorly encapsulated.
When not to create an abstraction
A premature abstraction is a shared surface created before the real common need is known. A speculative interface lists operations for imagined future requirements rather than present callers.
Suppose the product supports only plain text, and no caller needs to swap formats. Creating UniversalContentExporter with export(), compress(), addPassword(), upload(), and schedule() does not make the design ready for the future. It guesses five futures and makes today's code carry all five names.
Start with the concrete code when one small implementation tells the whole story. Extract an interface when a second real implementation reveals a stable shared operation, or when an existing part of the design genuinely benefits from a smaller contract. The timing is a trade-off: extracting too early freezes guesses; waiting too long lets format decisions spread through callers.
Do not judge an abstraction by how impressive its name sounds. Judge it by what knowledge it removes from the caller and whether each implementation can honor the contract without awkward exceptions.
Checkpoint
Answer all three to mark this lesson complete
You now have a way to name a stable “what” while several concrete types own different “hows.” Next, inheritance lets one class receive state and behavior from another, and you will examine both the convenience and the cost.