skip to content

Compare the object adapter (composition/delegation) and class adapter (inheritance) forms of the Adapter pattern. What are the trade-offs, and when is each even possible?

level: middleimportance: must knowfreq 55%

answer

  1. Class = is-a, Object = has-a
  2. Class adapter needs multiple inheritance / non-final adaptee
  3. Object adapter covers adaptee subclasses
  4. Composition → injectable, testable, encapsulated
  5. Two-way adapter ⇒ inheritance

basics

~20 s

A class adapter inherits from the adaptee and the target, translating inside itself. An object adapter holds an adaptee instance as a field and delegates. Composition is more flexible and works in single-inheritance languages, so it's the default.

solid answer

~50 s

Class adapter: the adapter *is-a* adaptee (inherits it) while also satisfying the target — one object, no extra indirection, and it can override protected/virtual adaptee members. But it binds to exactly one adaptee class at compile time, can't adapt that class's subclasses polymorphically, and needs multiple inheritance or an equivalent (interface + concrete superclass, mixins, traits, extension/inheritance of a non-final class) — impossible when the adaptee is final/sealed or when both target and adaptee are classes in a single-inheritance language. It also inherits the adaptee's entire public surface, leaking it to clients and creating a fragile base-class coupling. Object adapter: the adapter *has-a* adaptee; one adapter serves the adaptee and all its subtypes, the adaptee can be injected (testable, swappable at run time), several adaptees can be combined, and only the target's surface is exposed. Costs: one extra object and hop, and every method must be forwarded explicitly. Default to the object adapter; the modern "favor composition over inheritance" guidance is precisely this case.

code

pseudocode · 15 lines
pseudocode
// Adaptee (cannot be changed)
class LegacyReader { readLine(): String; eof(): Boolean }

// ---- CLASS ADAPTER: adapter IS-A adaptee ----
class ClassAdapter extends LegacyReader implements RecordSource {
  nextRecord() = if (eof()) null else parse(readLine())   // no field
}
// -> bound to LegacyReader forever; impossible if LegacyReader is final

// ---- OBJECT ADAPTER: adapter HAS-A adaptee ----
class ObjectAdapter implements RecordSource {
  constructor(private r: LegacyReader) {}
  nextRecord() = if (r.eof()) null else parse(r.readLine())
}
// -> works for LegacyReader and any subclass, injectable in tests

go deeper

for a junior

Name the two forms — inheritance vs holding a field — and say composition is the usual choice because it's more flexible.

for a middle

Give the concrete trade-off list: subclass coverage, run-time swapping/testing, API leakage, boilerplate, and when the class form is outright impossible (final/sealed adaptee, single inheritance).

for a senior

Add fragile-base-class and identity concerns, when overriding a protected hook justifies inheritance, two-way adapters, and how to cut forwarding boilerplate without losing type safety.

for a principal

Position it as an instance of 'favor composition over inheritance', discuss injection/lifecycle of adaptees, adapter chains and proxying strategies, and the maintenance economics of N adapters per adaptee.

## Setup Recall the participants: **Target** (interface the client expects), **Adaptee** (existing class with the wrong interface), **Adapter** (satisfies Target, uses Adaptee). The two forms differ only in *how the Adapter gets at the Adaptee*. ## Class adapter — inheritance ```pseudocode class TextFileAsRecordSource extends LegacyFileReader implements RecordSource { // inherited from LegacyFileReader: readLine(), eof(), openHandle() nextRecord() { if (this.eof()) return null return parse(this.readLine()) // no delegate field; calls itself } } ``` The adapter and adaptee are *one object*. In C++ this is literal multiple inheritance (public Target, public Adaptee). In single-inheritance languages (Java, C#, Kotlin, Swift) it works only when the Target is an *interface/protocol* and the Adaptee is an inheritable *class* — so it is really "extend the adaptee, implement the target". **Strengths** - **No forwarding boilerplate** for anything you don't need to change; inherited methods already satisfy matching parts of the Target. - **Can override** protected/virtual members of the adaptee — the only form that can reach into the adaptee's internals to fix behavior. - Marginally cheaper: one object, one dispatch, no delegate dereference. (Almost always irrelevant; occasionally matters in tight loops or embedded/allocation-sensitive code.) **Weaknesses** - **Compile-time binding to one concrete adaptee.** `TextFileAsRecordSource` adapts `LegacyFileReader` and *nothing else* — not its subclass `GzipFileReader`, not a test double. You need a new adapter class per adaptee. - **Impossible in common cases**: adaptee is `final`/`sealed`, its constructor is private/factory-only, both Target and Adaptee are classes, or the language forbids multiple inheritance. - **Leaks the adaptee's whole public API** to clients — they can call `readLine()` directly and bypass the translation, so the abstraction is not enforced. Any adaptee method added later silently appears on your adapter, possibly colliding with a Target method name. - **Fragile base class**: your adapter's correctness depends on the adaptee's internal call sequencing, which the vendor may change in a patch release. - **Identity confusion**: the adapter *is* an adaptee, so type checks, equality, serialization and framework scanning may treat it as one. ## Object adapter — composition ```pseudocode class TextFileAsRecordSource implements RecordSource { constructor(reader) { this.reader = reader } // Adaptee injected nextRecord() { if (this.reader.eof()) return null return parse(this.reader.readLine()) } } ``` **Strengths** - **Works for the adaptee and every subtype**, because the field is typed by the adaptee's abstraction. One adapter class, many adaptees. - **Run-time choice / dependency injection** — the adaptee is a constructor argument, so tests pass a stub and production passes the real thing; configuration can select among adaptees. - **Encapsulation**: clients see only the Target surface. You can add adapter-local state (buffers, caches, a mutex, a cursor) that inheritance would tangle with the adaptee's own state. - **Multiple adaptees**: one Target method may orchestrate calls across two or three collaborating adaptees. - Composes with other object adapters and decorators in a chain. **Weaknesses** - **Explicit forwarding** for every Target method — verbose for wide interfaces (some languages soften this with delegation keywords, e.g. Kotlin's `by`, or dynamic proxies/`__getattr__`-style forwarding). - Cannot override the adaptee's internals; you can only work with its public surface. - One more allocation and hop per call. ## Decision guide | Situation | Form | |---|---| | Adaptee is final/sealed, or both types are classes in a single-inheritance language | Object (class form impossible) | | Need to adapt a whole family/hierarchy of adaptees | Object | | Adaptee must be swappable at run time or faked in tests | Object | | Must override a protected/virtual hook of the adaptee to make it behave | Class | | Target is wide and mostly matches the adaptee already, adaptee is stable and yours | Class (least boilerplate) | | Default, no special reason | **Object** | ## Two-way adapters A **two-way (bidirectional) adapter** satisfies *both* interfaces so the object is usable as a Target by new clients and as an Adaptee by old ones. That is naturally a class adapter (it must genuinely be both types); with composition you'd need two wrapper objects and would lose object identity across the boundary. ## Pluggable adapters When a reusable component defines a narrow Target and expects callers to supply the translation, GoF calls it a **pluggable adapter**. Implementations: a small interface implemented per adaptee (object adapter), an abstract adapter class with default no-op methods, or first-class functions/lambdas — a single-method Target adapted by a closure is the most lightweight object adapter there is.

  • Your language has only single inheritance and the adaptee class is marked final/sealed. Which form can you use?
    Only the object adapter. You cannot extend a final class, so composition is the sole option — one reason composition is the practical default.
  • How would you avoid writing forwarding methods for a 30-method target interface?
    Use language delegation support (Kotlin `by`, C# explicit interface + a generated wrapper), a dynamic proxy / reflective interceptor that forwards by default, code generation, or an abstract adapter base with sensible defaults so each concrete adapter overrides only what differs. Each trades compile-time safety or clarity for brevity — worth it only for genuinely wide interfaces.
  • Does the object adapter form make the adaptee polymorphic, or the adapter?
    The adaptee. Because the field is typed by the adaptee's supertype, one adapter class serves the whole adaptee family. With a class adapter the *adapter* is fixed to one concrete adaptee, so you need N adapters for N adaptees.

Class adapter is a bilingual person who is both parties at once — efficient but they can only be that one pair of nationalities. Object adapter is a hired interpreter standing between them — one interpreter can serve any speaker of that language, and can be swapped for a different one mid-meeting.

saying these in an interview costs you the question

  • Claiming class adapters are always faster or better — the difference is one indirection, and the coupling cost dwarfs it.
  • Saying object adapters can't adapt subclasses — the reverse is true; class adapters are the ones locked to a single concrete adaptee.
  • Forgetting that a class adapter exposes the adaptee's entire public API, letting clients bypass the translation.
  • Asserting class adapters require multiple inheritance in every language — extending a class while implementing an interface works fine in single-inheritance languages.
  • Ignoring the fragile-base-class risk when inheriting from third-party code.

context