Object-Oriented Design

Abstraction and Interfaces

Depend on what something does, not how it does it. Interfaces name the behavior and hide the implementation.

Level: intermediate

What is it?

Abstraction separates what a component does from how it does it. An interface (or abstract class, protocol, or trait) declares a set of operations without implementing them. Callers depend on the interface; any class that fulfills it can be plugged in. The interface is the contract, and it is the part that must stay stable.

Why does it matter?

Real systems change: a database becomes a different database, a file becomes a network call. When callers depend on an interface, you swap the implementation without touching them, and you can substitute a fake in tests.

Mental model

A wall socket. Every appliance depends on the socket's shape, not on the power plant behind it. You can change the power plant, or plug in a generator, and nothing plugged in needs to know.

How it works

  • An interface is a contract: the operations, their inputs, outputs, and rules — not the code.
  • Program to an interface: parameters and fields should have the abstract type, the constructor picks the concrete one.
  • Abstract classes can share code among subclasses; interfaces (and protocols) only define the contract and allow many implementations.
  • Keep interfaces small and focused on what callers need. Wide interfaces leak implementation and are hard to satisfy.
  • Abstractions have a cost: indirection. Introduce them where change or testing actually needs a seam, not everywhere.

Steps

  • Name the behavior callers need
  • Declare it as an interface
  • Implement it in classes
  • Inject the implementation
  • Swap it without touching callers

Code example: Depend on the contract, plug in any implementation

from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def save(self, key, data): ...
    @abstractmethod
    def load(self, key): ...

class MemoryStorage(Storage):
    def __init__(self):
        self._d = {}
    def save(self, key, data):
        self._d[key] = data
    def load(self, key):
        return self._d[key]

def backup(store: Storage, key, data):    # knows only the interface
    store.save(key, data)

store = MemoryStorage()
backup(store, "note", "hello")
print(store.load("note"))                 # hello

Real-world usage

  • Repository, service, and gateway boundaries around databases, queues, and APIs
  • Plug-in architectures and drivers
  • Test doubles: swapping a real network call for an in-memory fake

Common mistakes

  • Creating an interface for every class, with exactly one implementation and no reason to change.
  • Interfaces that mirror one implementation's quirks.
  • Leaking implementation details, such as SQL rows, through the abstraction.

Interview questions

Abstract class versus interface?

An interface only declares behavior, and a class can implement many. An abstract class can also carry state and shared code but a class usually inherits from only one. Use an interface for a contract, an abstract class to share implementation among related classes.

What does 'program to an interface, not an implementation' mean?

Declare variables and parameters with the abstract type, so the code works with any implementation and the concrete class is chosen in one place.

What is a leaky abstraction?

One whose implementation details escape through the interface — error types, performance behavior, or data formats — so callers must know how it works underneath to use it correctly.

Prerequisites

Related concepts

Continue learning

Browse the full computer science library