Concepts / Structural patterns / Adapter

Adapter

Making an existing class fit the interface you need.

01The intuition

The problem it solves

A school attendance app stores records through a small interface it owns: save(record). The new biometric scanner from a vendor comes with its own library, and its method is called pushEvent, takes a JSON string, and uses a different date format.

class AttendanceStore:
    def save(self, record) -> None:
        self._rows.append(record)

class VendorScanner:                         # vendor code, cannot change
    def pushEvent(self, payload: str) -> int:
        print("vendor got", payload); return 200

def mark_present(student, when):
    record = {"id": student.id, "at": when.isoformat()}
    store.save(record)
    payload = json.dumps({"sid": student.id, "ts": when.strftime("%d/%m/%Y %H:%M")})
    scanner.pushEvent(payload)               # vendor's shape, leaked into our code

Every place that records attendance now knows the vendor's method name, argument shape and date format. When the vendor ships version 2 with sendEvents(list), every call site changes. And the app cannot treat the scanner like its other stores, because it does not speak save.

An adapter is a thin class that wraps something with the wrong interface and presents the interface you want. The old thing is unchanged; your code sees only your own interface.

Build it one piece at a time

Step 1: write down the interface your code wants. It already exists in spirit as save(record); make it explicit.

from abc import ABC, abstractmethod

class RecordSink(ABC):
    @abstractmethod
    def save(self, record: dict) -> None: ...

class AttendanceStore(RecordSink):
    def __init__(self): self._rows = []
    def save(self, record: dict) -> None:
        self._rows.append(record)

Step 2: wrap the vendor object in a class that implements your interface. All translation, names, formats, error codes, happens here and only here.

import json
from datetime import datetime

class ScannerSink(RecordSink):
    def __init__(self, scanner: VendorScanner):
        self._scanner = scanner
    def save(self, record: dict) -> None:
        when = datetime.fromisoformat(record["at"])
        payload = json.dumps({"sid": record["id"], "ts": when.strftime("%d/%m/%Y %H:%M")})
        status = self._scanner.pushEvent(payload)
        if status != 200:
            raise IOError(f"scanner rejected event: {status}")

Step 3: let the app talk to a list of sinks. The call site no longer knows the vendor exists.

class Attendance:
    def __init__(self, sinks: list[RecordSink]):
        self._sinks = sinks
    def mark_present(self, student, when) -> None:
        record = {"id": student.id, "at": when.isoformat()}
        for sink in self._sinks:
            sink.save(record)

attendance = Attendance([AttendanceStore(), ScannerSink(VendorScanner())])

Step 4: absorb the vendor's next version in the adapter. Version 2 wants batches; the adapter buffers and flushes, and the app is untouched.

class ScannerV2Sink(RecordSink):
    def __init__(self, scanner, batch: int = 20):
        self._scanner, self._batch, self._buf = scanner, batch, []
    def save(self, record: dict) -> None:
        self._buf.append(record)
        if len(self._buf) >= self._batch:
            self._scanner.sendEvents([json.dumps(r) for r in self._buf]); self._buf.clear()
StepPlaces that know the vendor's method namesVendor v2 editsCan tests run without the vendor library
Startevery call siteevery call siteno
1 and 2one adapterone adapteryes, with a fake RecordSink
3 and 4one adapterone new adapter classyes

Why it works

An adapter separates "what my program means" from "how this particular library says it". Your code depends on an interface you designed in your own vocabulary, so it stays stable while the outside world changes. The invariant is "foreign names and formats appear in adapters only". Because the adapter implements your interface, the foreign object becomes interchangeable with your own implementations, which is what lets the scanner sit in the same list as the store.

Where it breaks: an adapter that does real work. If the adapter starts validating business rules or caching, it has become a service and should be split. Keep it to translation and error mapping. Also watch the leaky adapter, one that exposes a vendor type in its own method signatures; then nothing was adapted.

The trade-off you must say out loud

An adapter is one more class per foreign thing and a small translation cost per call. It is worth it the moment the foreign interface might change, or you have two foreign things doing the same job, or you need to test without them. Say that boundary explicitly: "everything external comes in through an adapter to an interface I own". The follow-up is usually "what about data that does not map cleanly", and the answer is that the adapter is also where you decide what to do about it, drop, default or raise, in one place.

Where it shows up in the 100 Google problems

02Used in these problems