Menu
Coddy logo textTech

Adapter Pattern

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 97 of 104.

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two classes, converting the interface of one class into an interface that clients expect. This is particularly useful when integrating legacy code or third-party libraries that don't match your system's interface.

Imagine you have an existing class that does what you need, but its interface doesn't match what your code expects. Instead of modifying either side, you create an adapter that wraps the existing class and translates calls:

#include <iostream>
#include <string>

// Existing class with incompatible interface
class LegacyPrinter {
public:
    void printMessage(const std::string& msg) {
        std::cout << "Legacy: " << msg << "\n";
    }
};

// Target interface that client expects
class Printer {
public:
    virtual void print(const std::string& text) = 0;
    virtual ~Printer() = default;
};

// Adapter wraps LegacyPrinter and implements Printer interface
class PrinterAdapter : public Printer {
    LegacyPrinter& legacy;
public:
    PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
    
    void print(const std::string& text) override {
        legacy.printMessage(text);  // Translate the call
    }
};

int main() {
    LegacyPrinter oldPrinter;
    PrinterAdapter adapter(oldPrinter);
    
    // Client uses the expected interface
    adapter.print("Hello World");
}

The adapter holds a reference to the LegacyPrinter and implements the Printer interface. When print() is called, it delegates to printMessage(). The client code works with the Printer interface without knowing about the legacy implementation underneath.

Use Adapter when you want to use an existing class but its interface doesn't match what you need, or when you need to create a reusable class that cooperates with classes having incompatible interfaces.

challenge icon

Challenge

Easy

Let's build a Media Player Adapter system that bridges different audio library interfaces. Imagine you're integrating a legacy audio library into a modern media player application—the old library works perfectly, but its interface doesn't match what your player expects. This is exactly where the Adapter pattern shines.

You'll organize your code across four files:

  • MediaPlayer.h: Define the target interface that your modern media player expects.

    Create an abstract MediaPlayer class with these pure virtual methods:

    • play(const std::string& filename) — plays the specified file
    • stop() — stops playback
    • setVolume(int level) — sets volume (0-100)

    Include a virtual destructor.

  • LegacyAudioLib.h: Create the existing "legacy" audio library with an incompatible interface.

    Your LegacyAudioLib class represents an old library that works differently from what your player expects. It should have:

    • loadAudio(const std::string& path) — prints Legacy: Loading [path]
    • startPlayback() — prints Legacy: Playback started
    • stopPlayback() — prints Legacy: Playback stopped
    • adjustVolume(double percentage) — takes a decimal (0.0 to 1.0) and prints Legacy: Volume set to [percentage] (show the decimal value)
  • AudioAdapter.h: Build the adapter that bridges the two interfaces.

    Your AudioAdapter class should inherit from MediaPlayer and hold a reference to a LegacyAudioLib object. The adapter translates calls from the modern interface to the legacy one:

    • play() should call both loadAudio() and startPlayback() on the legacy library
    • stop() should delegate to stopPlayback()
    • setVolume() receives an integer (0-100) but the legacy library expects a decimal (0.0-1.0), so you'll need to convert it
  • main.cpp: Demonstrate the adapter in action.

    Read two inputs:

    1. A filename to play (string)
    2. A volume level (integer, 0-100)

    Create a LegacyAudioLib instance and an AudioAdapter that wraps it. Then use the adapter through the MediaPlayer interface (store it as a MediaPlayer pointer) to:

    1. Set the volume to the input level
    2. Play the input filename
    3. Stop playback

For example, with inputs song.mp3 and 75:

Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stopped

With inputs podcast.wav and 50:

Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stopped

Notice how the client code works entirely with the MediaPlayer interface—it has no idea that a legacy library is doing the actual work underneath. The adapter handles all the translation, including converting the integer volume to a decimal. This is the essence of the Adapter pattern: making incompatible interfaces work together without modifying either side.

Cheat sheet

The Adapter pattern allows incompatible interfaces to work together by acting as a bridge between two classes. It converts the interface of one class into an interface that clients expect.

Use the Adapter pattern when you want to use an existing class but its interface doesn't match what you need, or when integrating legacy code or third-party libraries.

Structure

The pattern consists of three main components:

  • Target Interface: The interface that the client expects
  • Adaptee: The existing class with an incompatible interface
  • Adapter: Wraps the adaptee and implements the target interface, translating calls between them

Example

#include <iostream>
#include <string>

// Existing class with incompatible interface (Adaptee)
class LegacyPrinter {
public:
    void printMessage(const std::string& msg) {
        std::cout << "Legacy: " << msg << "\n";
    }
};

// Target interface that client expects
class Printer {
public:
    virtual void print(const std::string& text) = 0;
    virtual ~Printer() = default;
};

// Adapter wraps LegacyPrinter and implements Printer interface
class PrinterAdapter : public Printer {
    LegacyPrinter& legacy;
public:
    PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
    
    void print(const std::string& text) override {
        legacy.printMessage(text);  // Translate the call
    }
};

int main() {
    LegacyPrinter oldPrinter;
    PrinterAdapter adapter(oldPrinter);
    
    // Client uses the expected interface
    adapter.print("Hello World");
}

The adapter holds a reference to the adaptee and implements the target interface. When methods are called on the adapter, it delegates to the adaptee's methods, translating the calls as needed. The client code works with the target interface without knowing about the legacy implementation.

Try it yourself

#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"

using namespace std;

int main() {
    // Read inputs
    string filename;
    int volumeLevel;
    cin >> filename;
    cin >> volumeLevel;
    
    // TODO: Create a LegacyAudioLib instance
    
    // TODO: Create an AudioAdapter that wraps the legacy library
    
    // TODO: Store the adapter as a MediaPlayer pointer
    
    // TODO: Use the MediaPlayer interface to:
    // 1. Set the volume to the input level
    // 2. Play the input filename
    // 3. Stop playback
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming