7 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Abstraction
1. What Is It?
is the OOP principle of exposing only what a component does — its and behavior — while hiding how it does it. A caller interacts with an without needing to know the underlying implementation details.
Without abstraction, every caller is coupled to implementation specifics. Change the implementation and every caller breaks. With abstraction, you define a stable contract (a class or method signature), and implementations can vary freely behind it. This is the mechanism that makes code replaceable, testable, and extensible.
A team builds a payment service that directly uses Stripe's API calls scattered throughout their checkout, order, and refund modules. Later, they need to switch to PayPal. What is the primary reason this change is painful?
2. How It Works
- Define the contract — an or declares what operations exist, with no implementation (or minimal shared logic).
- Implement concretely — one or more concrete classes fulfill the contract by providing the actual behavior.
- Program to the — callers depend on the abstract type, not the concrete class. At runtime, a concrete instance is injected or created, but the caller never changes.
Mermaid Diagram
Python
from abc import ABC, abstractmethod import math class Shape(ABC): @abstractmethod def area(self) -> float: """Return the area of the shape.""" ... @abstractmethod def perimeter(self) -> float: """Return the perimeter of the shape.""" ... def describe(self) -> str: return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}" class Circle(Shape): def __init__(self, radius: float) -> None: self._radius = radius def area(self) -> float: return math.pi * self._radius ** 2 def perimeter(self) -> float: return 2 * math.pi * self._radius class Rectangle(Shape): def __init__(self, width: float, height: float) -> None: self._width = width self._height = height def area(self) -> float: return self._width * self._height def perimeter(self) -> float: return 2 * (self._width + self._height) class ShapeRenderer: def render(self, shape: Shape) -> None: # Knows nothing about Circle or Rectangle — only the Shape contract print(f"Rendering: {shape.describe()}") renderer = ShapeRenderer() renderer.render(Circle(5.0)) renderer.render(Rectangle(4.0, 6.0))
Java
public abstract class Shape { public abstract double area(); public abstract double perimeter(); public String describe() { return String.format("%s: area=%.2f, perimeter=%.2f", getClass().getSimpleName(), area(), perimeter()); } } public class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } @Override public double perimeter() { return 2 * Math.PI * radius; } } public class Rectangle extends Shape { private final double width; private final double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } @Override public double perimeter() { return 2 * (width + height); } } public class ShapeRenderer { public void render(Shape shape) { // Depends only on Shape — not Circle or Rectangle System.out.println("Rendering: " + shape.describe()); } }
A ShapeRenderer class has a render(shape: Shape) method that accepts the abstract Shape type. A teammate suggests changing the method signature to render(circle: Circle) so the renderer can directly access Circle-specific fields. What is the primary drawback of this change?
3. Variants & Comparisons
Flavors of Abstraction
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Abstract class | Declares abstract methods; may provide shared state and default implementations | Shares common logic; carries state (fields) | Single inheritance limit; tighter coupling | Related types that share implementation |
| Interface | Pure contract; no state; Java 8+ allows default methods | Supports multiple inheritance; maximum flexibility | Cannot hold instance state or constructors | Unrelated types sharing a behavioral contract |
| Duck typing (Python) | No explicit contract; any object with the right method works | Zero boilerplate; maximally flexible | No compile-time safety; errors surface at runtime | Dynamic/scripting contexts; small codebases |
| Protocol (Python 3.8+) | typing.Protocol defines structural subtyping — static duck typing | Type-checker enforced without inheritance | Relatively new; less widely understood | Python codebases using mypy or pyright |
Language Constructs
- Python:
abc.ABC+@abstractmethodfor nominal ;typing.Protocolfor structural ; bare duck typing for informal contracts. - Java:
when shared /behavior is needed;(especially with Java 8+defaultmethods) for pure contracts. Key distinction: interfaces cannot hold instance variables or constructors.
You're building a backend system where a PaymentProcessor class needs to share state (like a transaction fee rate) and some default retry logic across multiple payment provider subclasses (Stripe, PayPal, etc.). Which abstraction mechanism is the best fit for this design?
4. When to Use It (and When NOT To)
Use abstraction when:
- Multiple concrete types share a common behavioral contract (shapes, payment methods, serializers).
- You need to swap implementations without changing callers (e.g., swap
FileLoggerforDatabaseLogger). - You're writing a library or framework where callers provide their own implementations.
- You want testability — inject a mock/stub that implements the same abstract type.
Anti-patterns:
- for one implementation: If there is exactly one class and no realistic second, the abstract layer is pure overhead. Add it when a second implementation appears.
- Leaky : The abstract exposes implementation-specific details (e.g., a
Shapethat hasgetSvgPath()— that's PDF vs SVG concern bleeding into the contract). - God abstraction: A single that all domain objects extend to "share everything" — it becomes a dumping ground, violating SRP.
Decision triggers:
- "I have 3+ implementations of the same behavior" → define an abstraction.
- "I need to inject a test double for this dependency" → extract an abstraction.
- "The caller shouldn't care which concrete class it's working with" → program to an abstraction.
- "I only have one implementation today but the spec says more are coming" → define the abstraction now.
A developer is building an internal reporting tool that currently generates only PDF reports. There is no mention of other formats in the requirements, but the developer creates an abstract ReportGenerator interface anyway, with a single concrete implementation PdfReportGenerator. Which of the following best describes this design decision?
5. Real-World Usage
1. java.io.InputStream (Java standard library)
InputStream is an . Concrete subclasses — FileInputStream, ByteArrayInputStream, BufferedInputStream — each override read() with storage-specific logic. Every Java API that reads bytes accepts InputStream, so callers are completely decoupled from the data source. Switching from file to in-memory buffer requires zero changes in the caller.
2. collections.abc (Python standard library)
Python's collections.abc module defines abstract base classes like Iterable, Sequence, and Mapping. Built-in types (list, tuple, dict) implement these contracts. Any code that accepts Iterable works with lists, generators, sets, and custom containers without modification. This is enabling the for loop to work uniformly across all sequence types.
3. Sorting / comparison APIs
Java's Comparator<T> and Python's key= callable parameter are both abstractions over comparison logic. Collections.sort(list, comparator) and sorted(items, key=fn) don't know what ordering they're applying — they call the . Custom sort orders (by price, by name, by distance) are plugged in without touching the sort algorithm.
A backend service reads configuration data from a file using a FileInputStream. A new requirement asks the service to also support reading configuration from an in-memory byte array during unit tests. If the service method accepts InputStream as its parameter type, what change is needed in the service method to support this new requirement?
6. Interview Cheat Sheet
Key sentences to say
- " separates what an object does from how it does it — callers depend on the contract, not the implementation."
- "In Java, I use an when related types need to share or partial implementation; I use an when I want a pure behavioral contract that unrelated types can fulfill."
- " is what makes and unit testing practical — you can swap a real implementation for a test double because they share the same abstract type."
- "The rule I follow: program to the most abstract type that satisfies the caller's needs — don't expose concrete classes where an will do."
- "Python's duck typing achieves abstraction implicitly — if it has the method, it satisfies the contract — but
typing.Protocolgives you that with static type-checking."
Common follow-up questions
Q: What's the difference between abstraction and ? A: hides internal behind methods to protect invariants. Abstraction hides implementation details behind an interface to decouple callers from specifics. Encapsulation is about data protection; abstraction is about reducing .
Q: When would you choose an over an interface in Java?
A: When the subtypes share concrete behavior or state. An abstract class can have fields and non-abstract methods — so if Animal needs a shared name field and a concrete sleep() method, an abstract class is appropriate. If I'm just defining a contract that unrelated types can fulfill (e.g., Comparable, Serializable), an interface is the right choice.
Q: Can you over-abstract? A: Yes. Premature abstraction creates indirection with no benefit — the YAGNI violation. The cost is indirection (harder to navigate, harder to understand). Add an abstraction when you have two implementations or a concrete need to swap, not speculatively.
Connections to other concepts
- depends on abstraction — you can only treat objects uniformly through a shared abstract type.
- Principle (DIP) formalizes abstraction as a rule: high-level modules must depend on abstractions, not concrete classes.
- pattern is abstraction applied to algorithms — the interface is the abstraction; concrete strategies are the implementations.
- pattern returns an abstract type, hiding which concrete class is instantiated.
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.