Loading content...
The Adapter Pattern can be implemented in two fundamentally different ways, each with distinct characteristics, constraints, and appropriate use cases. The class adapter uses inheritance to adapt one interface to another, while the object adapter uses composition.
This distinction isn't just an implementation detail — it reflects a deeper choice about how components relate to each other and how the system can evolve. Understanding both approaches, and knowing when to apply each, is essential for effective pattern application.
In languages that support only single inheritance (like Java, C#, TypeScript, and most modern languages), the choice between these approaches often determines what's even possible. In languages with multiple inheritance (like C++), you have more flexibility but also more complexity to manage.
Let's examine both approaches in depth.
By the end of this page, you will understand the mechanics of both class and object adapters, their structural differences, the tradeoffs each brings, language constraints that affect their applicability, and clear decision criteria for choosing between them.
The object adapter uses composition to connect the target interface to the adaptee. The adapter implements the target interface and holds a reference to an adaptee instance. When the client calls methods on the target interface, the adapter delegates to the adaptee through this reference.
Structural characteristics:
This is the most common and generally preferred form of the Adapter Pattern because it follows the principle of favoring composition over inheritance.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
// ═══════════════════════════════════════════════════════════════// OBJECT ADAPTER IMPLEMENTATION// ═══════════════════════════════════════════════════════════════ /** * Target Interface — what the client expects */interface MediaPlayer { play(filename: string): void; pause(): void; stop(): void; getPosition(): number;} /** * Adaptee — existing component with different interface * This represents an external library or legacy component */class AdvancedAudioEngine { private currentFile: string = ''; private playbackPosition: number = 0; private isPlaying: boolean = false; loadFile(audioPath: string): boolean { console.log(`Loading audio file: ${audioPath}`); this.currentFile = audioPath; this.playbackPosition = 0; return true; } startPlayback(): void { if (this.currentFile) { console.log(`Starting playback: ${this.currentFile}`); this.isPlaying = true; } } pausePlayback(): void { if (this.isPlaying) { console.log('Pausing playback'); this.isPlaying = false; } } stopPlayback(): void { console.log('Stopping playback'); this.isPlaying = false; this.playbackPosition = 0; } getCurrentTimestamp(): number { return this.playbackPosition; } seekTo(position: number): void { this.playbackPosition = position; }} /** * OBJECT ADAPTER — Uses composition * * Key characteristics: * - Implements Target interface * - Holds reference to Adaptee (composition) * - Delegates calls to adaptee through the reference * - Adaptee is injected (typically via constructor) */class AudioEngineAdapter implements MediaPlayer { // Composition: adapter CONTAINS adaptee private engine: AdvancedAudioEngine; constructor(engine: AdvancedAudioEngine) { // Adaptee is injected — enables flexibility this.engine = engine; } play(filename: string): void { // Translate: play() → loadFile() + startPlayback() this.engine.loadFile(filename); this.engine.startPlayback(); } pause(): void { // Direct delegation with different method name this.engine.pausePlayback(); } stop(): void { this.engine.stopPlayback(); } getPosition(): number { // Delegation with different method name return this.engine.getCurrentTimestamp(); }} // ═══════════════════════════════════════════════════════════════// CLIENT USAGE// ═══════════════════════════════════════════════════════════════ class MusicApp { constructor(private player: MediaPlayer) {} playTrack(track: string): void { console.log(`Playing track: ${track}`); this.player.play(track); }} // Composition at construction timeconst engine = new AdvancedAudioEngine();const adapter = new AudioEngineAdapter(engine);const app = new MusicApp(adapter); app.playTrack('song.mp3');The class adapter uses inheritance to connect the target interface to the adaptee. The adapter inherits from both the Target interface (or class) and the Adaptee class. This creates a class that IS-A Target and IS-A Adaptee simultaneously.
Structural characteristics:
Language constraint: True class adapters require multiple inheritance, which is available in C++ but not in Java, C#, or TypeScript. In single inheritance languages, the "class adapter" can only work if Target is an interface (which is usually the case for good design anyway).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
// ═══════════════════════════════════════════════════════════════// CLASS ADAPTER IMPLEMENTATION// (Partial in TypeScript since we can only extend one class)// ═══════════════════════════════════════════════════════════════ /** * Target Interface — what the client expects */interface MediaPlayer { play(filename: string): void; pause(): void; stop(): void; getPosition(): number;} /** * Adaptee — the class we're adapting */class AdvancedAudioEngine { protected currentFile: string = ''; protected playbackPosition: number = 0; protected isPlaying: boolean = false; loadFile(audioPath: string): boolean { console.log(`Loading audio file: ${audioPath}`); this.currentFile = audioPath; this.playbackPosition = 0; return true; } startPlayback(): void { if (this.currentFile) { console.log(`Starting playback: ${this.currentFile}`); this.isPlaying = true; } } pausePlayback(): void { if (this.isPlaying) { console.log('Pausing playback'); this.isPlaying = false; } } stopPlayback(): void { console.log('Stopping playback'); this.isPlaying = false; this.playbackPosition = 0; } getCurrentTimestamp(): number { return this.playbackPosition; }} /** * CLASS ADAPTER — Uses inheritance * * Key characteristics: * - Extends Adaptee class (inheritance) * - Implements Target interface * - Calls inherited methods directly (no delegation object) * - Cannot adapt subclasses of Adaptee without modification */class AudioEngineClassAdapter extends AdvancedAudioEngine implements MediaPlayer { // No adaptee reference needed — we inherit from it play(filename: string): void { // Call inherited methods directly (no this.adaptee needed) this.loadFile(filename); this.startPlayback(); } pause(): void { // Direct call to inherited method this.pausePlayback(); } stop(): void { this.stopPlayback(); } getPosition(): number { return this.getCurrentTimestamp(); } // BONUS: Class adapter can override adaptee behavior // This is not possible with object adapter startPlayback(): void { console.log('[Enhanced] Pre-loading audio buffer...'); super.startPlayback(); console.log('[Enhanced] Playback started with optimizations'); }} // ═══════════════════════════════════════════════════════════════// CLIENT USAGE — same as object adapter from client's perspective// ═══════════════════════════════════════════════════════════════ class MusicApp { constructor(private player: MediaPlayer) {} playTrack(track: string): void { this.player.play(track); }} // No separate adaptee instance — adapter is the adapteeconst adapter = new AudioEngineClassAdapter();const app = new MusicApp(adapter); app.playTrack('song.mp3');In single inheritance languages, if you extend the Adaptee, you cannot extend anything else. If your Target is a class (not an interface), true class adapters become impossible. This is why object adapters are far more common in languages like Java, C#, and TypeScript.
Let's visualize the fundamental structural difference between the two approaches.
Object Adapter Structure:
┌─────────────────┐ ┌─────────────────┐
│ Target │ │ Adaptee │
│ (interface) │ │ (class) │
├─────────────────┤ ├─────────────────┤
│ + request() │ │ + specificReq() │
└────────▲────────┘ └────────▲────────┘
│ │
│ implements │ contains
│ │
┌────────┴─────────────────────────┴────────┐
│ Adapter │
├────────────────────────────────────────────┤
│ - adaptee: Adaptee │
├────────────────────────────────────────────┤
│ + request() { │
│ this.adaptee.specificRequest(); │
│ } │
└────────────────────────────────────────────┘
Class Adapter Structure:
┌─────────────────┐ ┌─────────────────┐
│ Target │ │ Adaptee │
│ (interface) │ │ (class) │
├─────────────────┤ ├─────────────────┤
│ + request() │ │ + specificReq() │
└────────▲────────┘ └────────▲────────┘
│ │
│ implements │ extends
│ │
┌────────┴─────────────────────────┴────────┐
│ Adapter │
├────────────────────────────────────────────┤
│ (inherits adaptee members) │
├────────────────────────────────────────────┤
│ + request() { │
│ this.specificRequest(); // inherited │
│ } │
└────────────────────────────────────────────┘
| Aspect | Object Adapter | Class Adapter |
|---|---|---|
| Relationship to Adaptee | HAS-A (composition) | IS-A (inheritance) |
| Adaptee Instance | Separate object, held by reference | Adapter itself is the adaptee |
| Adaptee Access | Through interface/public methods only | All inherited members including protected |
| Method Invocation | Delegation through reference | Direct call to inherited methods |
| Runtime Flexibility | Adaptee can be swapped | Adaptee fixed at compile time |
| Inheritance Slot | Free (for other inheritance) | Consumed by Adaptee extension |
| Adaptee Subclasses | Works with any subclass automatically | Cannot adapt subclasses without new adapter |
The key insight: Object adapters use delegation while class adapters use inheritance. This mirrors the broader composition vs inheritance debate in object-oriented design. The same principles apply:
Understanding the nuanced tradeoffs helps you make informed decisions about which adapter style to use.
Tradeoff 1: Flexibility vs Simplicity
Object adapter can work with multiple adaptee implementations. If you have class AudioEngine, class PremiumAudioEngine extends AudioEngine, and class StreamingAudioEngine extends AudioEngine, a single object adapter class can adapt all of them if they share the interface being used.
Class adapter is locked to a single adaptee class. To adapt PremiumAudioEngine, you need a separate adapter class that extends it specifically.
Tradeoff 2: Coupling Tightness
Object adapter is loosely coupled to the adaptee. It depends only on the adaptee's public interface. If the adaptee's internal implementation changes but the public interface stays stable, the adapter doesn't need to change.
Class adapter is tightly coupled to the adaptee. It inherits everything — the entire implementation, not just the interface. Changes to the adaptee's protected members or internal structure can break the adapter.
Tradeoff 3: Behavior Modification
Class adapter can override adaptee methods:
class EnhancedAdapter extends Adaptee implements Target {
specificRequest(): void {
console.log('Pre-processing...');
super.specificRequest(); // Call original
console.log('Post-processing...');
}
request(): void {
this.specificRequest(); // Uses overridden version
}
}
Object adapter cannot override adaptee behavior (without subclassing, which defeats the purpose). It can only wrap and delegate:
class Adapter implements Target {
constructor(private adaptee: Adaptee) {}
request(): void {
// Can add pre/post logic around delegation
console.log('Pre-processing...');
this.adaptee.specificRequest(); // Cannot change what this does
console.log('Post-processing...');
}
}
Tradeoff 4: Access to Protected Members
Class adapter has full access to the adaptee's protected members:
class Adaptee {
protected internalState: string = 'state';
protected helperMethod(): void { /* ... */ }
}
class ClassAdapter extends Adaptee implements Target {
request(): void {
// Can access protected members
console.log(this.internalState);
this.helperMethod();
}
}
Object adapter can only access public members, which is generally better for encapsulation but sometimes limiting.
The choice between class and object adapters isn't purely about tradeoffs — language features often determine what's possible.
Single Inheritance Languages (Java, C#, TypeScript, Python, etc.)
In these languages, a class can extend only one other class. To create a class adapter:
This constraint makes object adapters the default choice in these languages. They work regardless of whether Target is an interface or class.
Multiple Inheritance Languages (C++)
C++ allows true class adapters that inherit from both Target and Adaptee:
// C++ class adapter with multiple inheritance
class Target {
public:
virtual void request() = 0;
};
class Adaptee {
public:
void specificRequest() { /* ... */ }
};
class Adapter : public Target, public Adaptee {
public:
void request() override {
specificRequest(); // Inherited from Adaptee
}
};
This creates a true IS-A relationship with both Target and Adaptee. However, multiple inheritance brings its own complexities (diamond problem, virtual inheritance, etc.).
| Language | Object Adapter | Class Adapter | Notes |
|---|---|---|---|
| TypeScript | ✅ Full support | ⚠️ Limited (Target must be interface) | Single class inheritance |
| Java | ✅ Full support | ⚠️ Limited (Target must be interface) | Single class inheritance |
| C# | ✅ Full support | ⚠️ Limited (Target must be interface) | Single class inheritance |
| Python | ✅ Full support | ✅ Full support | Multiple inheritance supported |
| C++ | ✅ Full support | ✅ Full support | Multiple inheritance with complexities |
| Go | ✅ Via embedding | ❌ No inheritance | Composition is idiomatic |
| Rust | ✅ Via traits | ❌ No inheritance | Trait-based abstraction |
In practice, 95%+ of adapter implementations you'll encounter in Java, C#, and TypeScript are object adapters. The language constraint combined with good design practice (preferring composition, designing to interfaces) makes this the natural choice. Understanding class adapters remains valuable for C++ work and for appreciating the pattern's full design space.
Given the tradeoffs and constraints, how do you decide which adapter style to use? Here's a systematic decision framework.
Default to Object Adapter unless you have a specific reason not to.
Object adapters are more flexible, more loosely coupled, work in all languages, and align with modern composition-over-inheritance principles. Start with this assumption and only consider class adapters if specific requirements push you there.
Consider Class Adapter when:
You need to override adaptee behavior — If part of the adaptation requires changing how the adaptee works (not just translating the interface), class adapter's ability to override methods is valuable.
You need access to protected members — If the translation requires information or operations that are only available through protected (not public) members, class adapter gives you that access.
The adaptee is final/sealed — Wait, this actually prevents class adapters! If the adaptee cannot be extended, object adapter is your only option.
Performance is critical — In extremely performance-sensitive scenarios, the direct method call of class adapters might matter. This is rare; profile before assuming.
12345678910111213141516171819202122232425262728293031323334353637383940414243
ADAPTER TYPE DECISION FLOWCHART═══════════════════════════════════════ START │ ▼Is Target an interface (not a class)? │ ├─ NO ──► Use OBJECT ADAPTER (only option) │ ▼ YES │Can the Adaptee be extended (not final/sealed)? │ ├─ NO ──► Use OBJECT ADAPTER (only option) │ ▼ YES │Do you need to override Adaptee methods? │ ├─ YES ──► Consider CLASS ADAPTER │ ▼ NO │Do you need access to protected members? │ ├─ YES ──► Consider CLASS ADAPTER │ ▼ NO │Will you need to adapt subclasses of Adaptee? │ ├─ YES ──► Use OBJECT ADAPTER │ ▼ NO │Is runtime adaptee selection needed? │ ├─ YES ──► Use OBJECT ADAPTER │ ▼ NO │Either works — prefer OBJECT ADAPTER for flexibilityIf you're unsure which to use and both are possible, choose the object adapter. Composition keeps your options open. You can always switch from composition to inheritance later if needed, but the reverse is harder. Object adapters are more testable (easier to inject mocks), more flexible (can swap adaptees), and create cleaner dependency graphs.
Beyond the theoretical tradeoffs, several practical considerations affect adapter choice in real projects.
Third-Party Libraries
When adapting third-party code, you typically cannot extend their classes (they may be final, or you don't want tight coupling to their implementations). Object adapters are the natural choice — they depend only on the library's public interface and provide insulation from library updates.
Testing Strategy
Object adapters are easier to test:
// Object adapter — easy to test with mock
it('should translate requests', () => {
const mockAdaptee = { specificRequest: jest.fn() };
const adapter = new ObjectAdapter(mockAdaptee);
adapter.request();
expect(mockAdaptee.specificRequest).toHaveBeenCalled();
});
// Class adapter — harder to isolate from adaptee
it('should translate requests', () => {
const adapter = new ClassAdapter(); // Cannot mock the parent
adapter.request(); // Calls real adaptee code
// Limited ability to verify behavior
});
Dependency Injection Compatibility
Modern applications use dependency injection containers that work naturally with object adapters:
// DI container configuration
container.bind<Adaptee>().to(ConcreteAdaptee);
container.bind<Target>().to(ObjectAdapter); // Adaptee injected automatically
// Class adapter doesn't fit this pattern as naturally
Framework Considerations
Some frameworks provide their own adaptation mechanisms:
Understanding the adapter pattern helps you recognize and effectively use these framework facilities.
Evolution and Maintenance
Object adapters handle system evolution better:
Class adapters create tighter coupling that makes these scenarios more complex.
We've thoroughly compared class and object adapters. Let's consolidate the key insights.
What's next:
We've covered the Adapter Pattern's problem, solution, and implementation variations. The next page brings it all together with comprehensive real-world use cases and examples. We'll see the pattern applied to logging frameworks, payment gateways, legacy system integration, and more — demonstrating both adapter styles in practical contexts.
You now understand both class and object adapters in depth. You can identify the structural differences, articulate the tradeoffs, recognize language constraints, and apply a clear decision framework to choose the right approach for your situation. Next, we'll explore real-world applications of the Adapter Pattern.