OOP

Abstraction

Abstraction exposes only the essential features of something while hiding the complex details, letting you work with high-level concepts instead of low-level mechanics.

What, not how.

Abstraction means focusing on what an object does rather than how it does it. When you drive a car, you use the steering wheel and pedals — you don't need to know how the engine combusts fuel. The controls are an abstraction over the machinery.

In code, you call list.sort() without knowing the sorting algorithm, or save(user) without knowing the SQL. Abstraction is achieved with abstract classes and interfaces that define what operations exist, leaving the how to implementations.

bad.py
# Problem: callers must know storage details (the "how").
def save_user(user):
    sql = f"INSERT INTO users (name) VALUES ('{user.name}')"
    db = open_postgres_connection("prod-db")
    db.execute(sql)
    db.commit()
# Swap to Mongo? Every call site must change.
good.py
# Fix: expose what (save), hide how (SQL, HTTP, file…).
from abc import ABC, abstractmethod

class UserStore(ABC):
    @abstractmethod
    def save(self, user):
        """Persist a user. Callers don't care about the backend."""

class PostgresUserStore(UserStore):
    def save(self, user):
        # SQL details stay inside this class only.
        ...

class InMemoryUserStore(UserStore):
    def save(self, user):
        self._rows.append(user)   # fine for tests

def register(store: UserStore, user):
    store.save(user)              # abstraction: only "save" matters

Interfaces and ADTs.

  • Interfaces / abstract classes — declare capabilities without implementation.
  • Abstract data types — a List or Map defined by operations, not representation.
  • Levels of abstraction — code reads top-down: high-level intent calls mid-level steps calls low-level details.
Abstraction vs encapsulation Abstraction is a design activity (deciding what to expose); encapsulation is the mechanism that hides the rest. You use encapsulation to implement an abstraction.

Leaky abstractions.

  • Leaky abstraction — details you tried to hide still surface (performance, errors), forcing callers to know the internals.
  • Wrong abstraction — a premature or ill-fitting abstraction can be costlier than duplication; sometimes you should inline it back.
  • Stable interfaces — a good abstraction changes far less often than its implementations.
  • Enabling polymorphism & DIP — abstractions are the seams that let you swap implementations and invert dependencies.
"All non-trivial abstractions leak." — Joel Spolsky. Abstractions save time but never fully hide the layer beneath, especially under failure or load.

Interview Questions

Filter

The principle of exposing only the essential features of something and hiding the underlying complexity, so you interact with a simple high-level concept rather than the low-level details.

A TV remote: pressing "volume up" abstracts away the electronics inside. Or a car's pedals and wheel — you operate the car through a simple interface without understanding the engine, transmission, or braking systems.

Primarily through abstract classes and interfaces, which declare what operations are available (method signatures) without specifying how they work, leaving implementation to concrete subclasses.

A method declared without an implementation (just a signature) in an abstract class or interface. Concrete subclasses must provide the actual implementation. It defines a required capability without dictating how it's done.

Yes. A function name and its parameters abstract away its internal steps; an API/library exposes operations while hiding implementation. Abstraction isn't unique to OOP — any interface that lets you use something without knowing its internals is an abstraction.

It reduces complexity (you reason about high-level concepts), enables reuse and swapping of implementations behind a stable interface, improves maintainability, and lets different parts of a system evolve independently. It's how large systems stay comprehensible.

Abstraction is one of the four pillars (alongside encapsulation, inheritance, and polymorphism). It focuses on modeling essential characteristics and hiding irrelevant detail at the design level.

No. An abstract class is incomplete (it has unimplemented abstract methods), so you can't instantiate it directly. You instantiate a concrete subclass that implements the missing methods, though you can reference it through the abstract type.

Abstraction is a design concern — deciding what essential concepts to expose and hiding complexity (the "what"). Encapsulation is the implementation mechanism — bundling data with behavior and restricting access to internals (the "how it's protected"). You design an abstraction (interface) and use encapsulation (access modifiers) to hide the details that realize it. Abstraction hides complexity; encapsulation hides state.

An abstraction that fails to fully hide the details beneath it, so callers must understand the underlying implementation to use it correctly. Example: an ORM abstracts SQL, but you still hit N+1 query problems or must reason about generated SQL for performance. Network transparency leaks when latency/failures force callers to handle them. Leaky abstractions aren't necessarily bad — they're often unavoidable — but you should be aware of what leaks and design so the common case stays simple.

A data type defined by its operations and behavior rather than its implementation. A Stack ADT is defined by push/pop/peek regardless of whether it's backed by an array or linked list; a Map ADT by get/put regardless of hash table vs tree. The ADT is the abstraction; the data structure is the implementation. This lets you swap implementations without changing code that uses the ADT.

Depending on abstractions (interfaces) rather than concrete implementations lets you substitute test doubles (mocks/stubs/fakes) for real collaborators — e.g. a fake PaymentGateway or in-memory UserRepository. This isolates the unit under test, avoids slow/external dependencies (network, DB), and makes tests deterministic. The abstraction is the seam where you inject a controllable replacement.

Code operates at different altitudes: high-level policy ("process the order"), mid-level steps ("validate", "charge", "ship"), and low-level details (string parsing, index math). A function should stay at one level of abstraction — mixing a high-level orchestration with low-level byte manipulation makes it hard to read. Keeping each function at a single level (extracting lower-level detail into helper methods) makes code read like a top-down narrative and is easier to understand and change.

Abstraction hides detail to expose essentials (ignoring what's irrelevant for the current purpose). Generalization extracts common features from multiple specific things into a shared, broader concept (e.g. Cat and Dog → Animal). Generalization often produces abstractions (a general base type), but abstraction is the broader idea of simplifying by omitting detail, while generalization specifically means unifying commonalities into a more general type.

DIP says high-level modules and low-level modules should both depend on abstractions, not on each other. Abstractions (interfaces) are exactly the "seam" DIP relies on: the high-level policy depends on an interface, and the low-level detail implements it, so dependencies point toward the abstraction. This decouples layers and lets you swap implementations (e.g. a real vs mock database) without changing high-level code. Abstraction makes inversion possible.

Each layer (presentation, application/business, data access) exposes an abstract interface to the layer above and hides how it fulfills requests. The business layer calls a repository interface without knowing about SQL or the database; the presentation layer calls services without knowing business rules' internals. These abstraction boundaries let each layer change independently and be tested in isolation, and they enforce a clean dependency direction.

Yes. Over-abstraction adds interfaces, layers, and indirection that aren't needed, making code harder to follow and change (you must jump through many files to understand one behavior). Signs: interfaces with a single implementation created "just in case," deep hierarchies, speculative generality (YAGNI violations), and abstractions that don't match real variation. The remedy is to abstract in response to actual, demonstrated need (e.g. the rule of three) rather than anticipated flexibility.

Joel Spolsky's law: "all non-trivial abstractions, to some degree, are leaky." No abstraction perfectly hides the layer beneath; details leak through under edge cases, failures, or performance concerns (TCP abstracts reliable delivery but latency/timeouts leak; an ORM leaks SQL performance; a filesystem leaks disk failures). Practical implications: you can't fully escape understanding the lower layers when debugging or optimizing; abstractions speed up the common case but you must be ready to "look underneath" for the hard 20%; and adding layers doesn't eliminate complexity, it relocates it. Design so the common path is simple and leaks are observable and handleable.

Sandi Metz's point: when you prematurely unify things that only look similar, the shared abstraction accumulates conditionals and special cases as the paths diverge, becoming a tangled, coupled mess that's harder to change than the original duplication. Because everything depends on it, fixing it is risky. The healthier move is often to inline the abstraction back (re-duplicate) and then let the correct abstraction emerge from real, repeated patterns. Duplication is cheaper to fix than the wrong coupling; prefer waiting until the true abstraction is clear.

Abstractions can add virtual/dynamic dispatch (indirect calls that hinder inlining), extra allocations and indirection (wrapper objects, boxing), and layers that each copy/transform data. In hot paths this matters. Manage it by: measuring before optimizing; keeping abstractions thin; using techniques like inlining, devirtualization (JIT/compiler), value types, and generics/templates (compile-time polymorphism avoids runtime dispatch); and choosing zero-cost abstractions where available (C++/Rust). The principle: keep clean abstractions everywhere, then selectively collapse them only in profiled hot spots — don't sacrifice design globally for micro-optimizations that don't matter.

A good abstraction is stable (its interface changes far less than implementations), minimal (small, cohesive surface exposing intent, hiding representation), honest (doesn't leak more than necessary; the contract matches behavior including errors), and aligned with real variation (the things it hides are the things that actually vary). Design it by understanding the domain and the axes of change, expressing operations in the domain's language (not implementation terms), depending on it from clients, and validating with at least a couple of genuinely different implementations. Deep modules (simple interface, substantial hidden functionality) are ideal, per Ousterhout.

Abstraction inversion occurs when an abstraction hides functionality that users actually need, forcing them to laboriously reconstruct it from the exposed primitives — and then higher layers get built on those reconstructions. Example: a library exposes only high-level operations internally implemented with a simpler primitive it doesn't expose, so clients reimplement the primitive on top of the high-level API. It signals a poorly-chosen abstraction boundary. The fix is to expose the appropriate lower-level operations (or the missing capability) so users don't fight the abstraction.

Polymorphism requires a common abstract type (interface/abstract class) through which different concrete implementations are used interchangeably. The abstraction defines the shared contract (method signatures); callers program against it, and at runtime the actual object's overridden methods execute (dynamic dispatch). Without the abstraction there's no common type to substitute implementations for. So abstraction provides the "one interface" and polymorphism provides the "many implementations behind it" — abstraction is the enabling structure, polymorphism the behavior it unlocks.

Each service abstracts its data and logic behind an API/contract, hiding its database and implementation so others depend only on the interface. But distribution leaks the abstraction: network latency, partial failures, timeouts, retries/idempotency, versioning, and eventual consistency all surface to callers in ways an in-process method call wouldn't (the fallacies of distributed computing). So a remote call can't be treated as a transparent local call. Good design makes the contract explicit about these realities (timeouts, error semantics, async where appropriate), keeps interfaces coarse-grained to reduce chatty coupling, and versions contracts carefully — accepting that the network boundary is a leaky abstraction that must be designed for, not hidden.

YAGNI ("you aren't gonna need it") warns against speculative abstractions built for imagined future needs, which add complexity now for uncertain payoff. The rule of three suggests waiting until you see a pattern repeated ~three times before extracting an abstraction, so it's grounded in real, observed variation rather than guesswork. Balance: keep code concrete and simple until duplication or a genuine need for a seam (e.g. testability, a second real implementation) appears, then refactor toward the abstraction that the actual cases reveal. This yields abstractions that fit reality and avoids both premature over-engineering and letting harmful duplication fester.

From Ousterhout's "A Philosophy of Software Design": a deep module has a simple interface hiding a lot of functionality (great abstraction — high benefit, low cost to use), while a shallow module has an interface nearly as complex as its implementation (little abstraction benefit, just added indirection). Good abstractions are deep: they hide substantial complexity behind a small, clean interface. Shallow modules (thin wrappers, pass-through layers, classes with one trivial method) increase cognitive load without hiding much, so they're often not worth their cost. Aim to maximize the ratio of hidden complexity to interface complexity.

Treat the interface as a contract. Add capabilities via new methods/overloads or new interfaces rather than changing existing signatures; use default methods (Java 8+) or adapter/wrapper layers to extend without breaking implementers. Deprecate before removing, giving clients a migration path and window. Version the abstraction when a breaking change is unavoidable, running old and new in parallel. Keep the interface minimal from the start so there's less to change, and hide volatile details so most changes stay behind the abstraction. When the abstraction itself is wrong, sometimes the right move is to introduce a new, better abstraction alongside and migrate incrementally rather than mutate the old one under everyone's feet.

An interface is a pure abstraction — a contract of method signatures with (traditionally) no state or implementation — and a class can implement many. An abstract class can provide partial implementation and shared state alongside abstract methods, but a class extends only one. Use an interface to define a capability many unrelated types can offer (abstraction over behavior); use an abstract class to share common code among closely related types while still abstracting the varying parts.

Well-chosen abstractions let a developer understand and work on one part of the system without holding the whole thing in their head — they learn a module's interface and trust the rest behind their boundaries. Clear layer and module boundaries create a mental map ("this is where persistence lives, this is where business rules live"), and stable interfaces mean local changes don't require system-wide knowledge. Poor abstractions (leaky, shallow, or inconsistent) force developers to understand internals everywhere, dramatically raising the learning curve.

Aim for the level that matches how clients think about the problem and gives them power without forcing them to manage details they don't care about — but also without hiding capabilities they legitimately need (avoiding abstraction inversion). Offer high-level convenience operations for the common case while exposing lower-level building blocks for advanced use (layered API design), so simple things are simple and complex things are possible. Keep the surface minimal and stable, use the domain's vocabulary, be explicit about error and performance semantics, and design against real client use cases rather than internal structure. Validate by prototyping how actual consumers would use it and iterating before the API ossifies.