Adapter Pattern


  • Description: Wrap an incompatible interface so client code can talk to it through the interface it already expects — object-adapter via composition or class-adapter via multiple inheritance.
  • My Notion Note ID: K2C-2-6
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Intent

  • Bridge mismatched interfaces without modifying either side.
  • Classic use → legacy lib has API A, client expects API B → Adapter sits between, exposes B, delegates to A.
  • Also called Wrapper.

Two flavors (GoF):

  • Object adapter → holds adaptee by composition (pointer / reference / value). Most common in modern C++.
  • Class adapter → inherits from both target interface and adaptee (multiple inheritance). Compile-time, no indirection cost, but rigid.

2. Structure

Role Responsibility
Target Interface the client expects (B).
Adaptee Existing class with the incompatible interface (A).
Adapter Implements Target, holds (or inherits) Adaptee, translates calls.
Client Depends only on Target. Unaware of Adaptee.

Object adapter → Adapter has-a Adaptee. Class adapter → Adapter is-a Target and is-a Adaptee.


3. C++ Example

Scenario → modern app expects IRenderer::draw(Shape), legacy lib exposes LegacyDraw::render_xy(int, int, int, int).

3.1 Object Adapter (composition)

#include <memory>
#include <iostream>

// Target — what the client expects
struct Shape { int x, y, w, h; };

class IRenderer {
public:
    virtual ~IRenderer() = default;
    virtual void draw(const Shape& s) = 0;
};

// Adaptee — incompatible legacy API
class LegacyDraw {
public:
    void render_xy(int x1, int y1, int x2, int y2) {
        std::cout << "legacy: " << x1 << "," << y1
                  << " -> " << x2 << "," << y2 << "\n";
    }
};

// Adapter — composes the adaptee
class LegacyRendererAdapter : public IRenderer {
public:
    explicit LegacyRendererAdapter(std::shared_ptr<LegacyDraw> impl)
        : impl_(std::move(impl)) {}

    void draw(const Shape& s) override {
        impl_->render_xy(s.x, s.y, s.x + s.w, s.y + s.h);
    }
private:
    std::shared_ptr<LegacyDraw> impl_;
};

void client(IRenderer& r) { r.draw({10, 20, 100, 50}); }

int main() {
    auto adapter = LegacyRendererAdapter{std::make_shared<LegacyDraw>()};
    client(adapter);
}

3.2 Class Adapter (multiple inheritance)

class LegacyRendererClassAdapter : public IRenderer, private LegacyDraw {
public:
    void draw(const Shape& s) override {
        render_xy(s.x, s.y, s.x + s.w, s.y + s.h);
    }
};
  • private inheritance from LegacyDraw → adapter IS-A Target but only IMPLEMENTED-IN-TERMS-OF Adaptee.
  • Cannot adapt a final adaptee. Cannot adapt at runtime — picked at compile time.

3.3 Free-function / lambda adapter

For one-shot adaptation of a single call → wrap in a lambda. Cheaper than a class.

auto draw = [adaptee = LegacyDraw{}](const Shape& s) mutable {
    adaptee.render_xy(s.x, s.y, s.x + s.w, s.y + s.h);
};

Useful with std::function, range adaptors, callbacks.


4. When to Use

Use when:

  • Reusing a class whose interface doesn't match what the surrounding code expects.
  • Wrapping a C library behind a C++ interface.
  • Adapting third-party SDK to a domain abstraction (so swapping the SDK later is one-file change).
  • Conforming several incompatible classes to a common interface so they can be used polymorphically.

Avoid when:

  • You control both sides → just fix the interface.
  • The "adapter" is doing real work beyond translation → that's a Facade or domain wrapper, not Adapter.
  • Translation requires runtime state machines, retries, caching → consider Decorator or a separate service layer.

5. Variants and Pitfalls

  • Two-way adapter → implements both interfaces, lets either side talk to either. Rarely needed.
  • Pluggable adapter → adaptee chosen at runtime via constructor injection. Default for testability.
  • Static / template adapter → CRTP or concept-based, compile-time dispatch. Zero virtual cost.

Pitfalls:

  • Lifetime ownership. Adapter holding a raw pointer to an adaptee owned elsewhere → dangling. Prefer shared_ptr / unique_ptr or by-value.
  • Leaky abstraction. Adapter exposes adaptee-specific error codes / types → client now coupled to adaptee anyway.
  • Adapter explosion. N targets × M adaptees = N×M adapters. Sign that the underlying abstraction is wrong.
  • Class-adapter MI hazards. Diamond problems if both bases share an ancestor; harder to mock; cannot adapt final classes.
  • Over-adapting trivial calls. A single function call doesn't justify a class — use a lambda.

6. Related Patterns

  • Adapter vs Facade. Adapter exposes the same shape of operations through a different interface → 1-to-1 translation. Facade introduces a new, simpler interface over a subsystem of many classes — interfaces deliberately differ. Facade is about scope reduction; Adapter is about interface translation.
  • Adapter vs Decorator. Decorator preserves the wrapped interface — same Target in and out, adds behavior. Adapter changes the interface, adds little or no behavior.
  • Adapter vs Proxy. Proxy keeps the same interface as the subject, controls access (lazy load, remote, permissions). Adapter changes the interface.
  • Adapter vs Bridge. Bridge is designed up front to separate abstraction from implementation; Adapter is retrofitted after the fact to fit two existing pieces together.
  • Strategy. Adapter often wraps a Strategy when the strategy interface doesn't match the third-party algorithm's API.

7. References