Skip to content

Adapter Pattern

Intent

The Adapter pattern converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

Also known as: Wrapper

Problem

You want to use an existing class, but its interface doesn't match the one you need. This commonly occurs when:

  • You need to integrate a third-party library with an incompatible interface
  • You want to reuse legacy code in a new system
  • You need to create a reusable class that cooperates with unrelated or unforeseen classes

Solution

The Adapter pattern suggests creating a wrapper class (the Adapter) that translates the interface of one class (the Adaptee) into an interface expected by the client (the Target). The adapter implements the Target interface and translates calls to the Adaptee's interface.

Structure

Object Adapter (using composition)

Adapter Pattern Diagram 0

Class Adapter (using multiple inheritance)

Adapter Pattern Diagram 1

Participants

  • Target - Defines the domain-specific interface that Client uses - In our example: Target class with request() method
  • Client - Collaborates with objects conforming to the Target interface - In our example: clientCode() function
  • Adaptee - Defines an existing interface that needs adapting - In our example: Adaptee class with specificRequest() method
  • Adapter - Adapts the interface of Adaptee to the Target interface - In our example: Adapter (object adapter) and ClassAdapter (class adapter)

Implementation

Object Adapter

class Adapter : public Target {
private:
    std::unique_ptr<Adaptee> adaptee_;

public:
    explicit Adapter(std::unique_ptr<Adaptee> adaptee) 
        : adaptee_(std::move(adaptee)) {}

    std::string request() const override {
        std::string toReverse = this->adaptee_->specificRequest();
        std::reverse(toReverse.begin(), toReverse.end());
        return "Adapter: (TRANSLATED) " + toReverse;
    }
};

Class Adapter

1
2
3
4
5
6
7
8
class ClassAdapter : public Target, private Adaptee {
public:
    std::string request() const override {
        std::string toReverse = this->specificRequest();
        std::reverse(toReverse.begin(), toReverse.end());
        return "ClassAdapter: (TRANSLATED) " + toReverse;
    }
};

Usage Example

// Client code works with Target interface
void clientCode(const Target* target) {
    std::cout << target->request() << std::endl;
}

int main() {
    // Adaptee with incompatible interface
    auto adaptee = std::make_unique<Adaptee>();

    // Object Adapter
    auto adapter = std::make_unique<Adapter>(std::move(adaptee));
    clientCode(adapter.get());

    // Class Adapter
    auto classAdapter = std::make_unique<ClassAdapter>();
    clientCode(classAdapter.get());

    return 0;
}

Applicability

Use the Adapter pattern when:

  • You want to use an existing class with an incompatible interface - The adapter provides the interface clients expect while using the existing class
  • You want to create a reusable class that cooperates with unrelated classes - The adapter can work with classes that don't necessarily have compatible interfaces
  • You need to use several existing subclasses but can't adapt their interface by subclassing - Object adapter can adapt the interface of its parent class

Consequences

Benefits

  1. Single Responsibility Principle - You can separate the interface or data conversion code from the primary business logic

  2. Open/Closed Principle - You can introduce new types of adapters without breaking existing client code

  3. Flexibility - Object adapter can adapt many adaptees (the adaptee and all its subclasses)

  4. Reusability - Can reuse existing functionality without modifying the source code

Drawbacks

  1. Increased complexity - Overall complexity increases because you need to introduce new interfaces and classes

  2. Performance overhead - Additional layer of indirection (though usually negligible)

Object Adapter vs Class Adapter

Object Adapter (Composition)

Advantages: - Can adapt the adaptee and all its subclasses - More flexible (can switch adaptees at runtime) - Follows composition over inheritance principle

Disadvantages: - Cannot override Adaptee's behavior - Slightly more complex implementation

Class Adapter (Multiple Inheritance)

Advantages: - Can override Adaptee's behavior - No extra pointer indirection - Slightly more efficient

Disadvantages: - Only adapts the specific Adaptee class - Not available in languages without multiple inheritance (Java, C#) - Tighter coupling between Adapter and Adaptee

Real-world Examples

  1. Legacy System Integration - Adapting old APIs to work with modern code - Example: Wrapping a legacy payment system to work with a new e-commerce platform

  2. Third-party Library Integration - Making external libraries compatible with your interface - Example: Adapting different logging libraries to a common logging interface

  3. Data Format Conversion - Converting data between different formats - Example: XML to JSON adapter, or metric to imperial unit adapter

  4. GUI Toolkits - Adapting platform-specific UI components to a common interface - Example: Qt, GTK adapters for different operating systems

  5. Database Drivers - Providing a uniform interface to different database systems - Example: ODBC/JDBC drivers adapt various databases to standard interfaces

  • Bridge: Similar structure but different intent. Bridge separates interface from implementation to allow both to vary independently, while Adapter makes existing interfaces work together.
  • Decorator: Changes an object's responsibilities without changing its interface, while Adapter changes the interface.
  • Proxy: Provides the same interface as the subject, while Adapter provides a different interface.
  • Facade: Defines a new interface for a set of objects, while Adapter reuses an old interface.

Implementation Considerations

  1. Choose between Object and Class Adapter - Object adapter is more flexible and widely applicable - Class adapter is useful when you need to override adaptee behavior

  2. Two-way adapters - Can be bidirectional, allowing the adapted object to be used through both interfaces - Useful for gradual migration from old to new interfaces

  3. Pluggable adapters - Design adapters to be easily replaceable - Use interfaces and dependency injection

  4. Minimizing adaptation - Keep the amount of work the adapter has to do minimal - Consider if the adaptee interface could be changed instead

Sample Output

=== Adapter Pattern Demo ===

Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS
(Note: The text is reversed!)

Client: But I can work with it via the Adapter (Object Adapter):
Adapter: (TRANSLATED) Special behavior of the Adaptee.

Client: I can also work with ClassAdapter (Class Adapter):
ClassAdapter: (TRANSLATED) Special behavior of the Adaptee.

=== Real-world Scenario ===

Scenario: Legacy system integration
- New system expects Target interface
- Legacy system provides Adaptee interface
- Adapter bridges the gap without modifying either system

Legacy System Output: .eetpadA eht fo roivaheb laicepS
Adapted for New System: Adapter: (TRANSLATED) Special behavior of the Adaptee.

Key Takeaways

  1. Adapter makes incompatible interfaces work together
  2. Two implementation approaches: object adapter (composition) and class adapter (inheritance)
  3. Object adapter is more flexible and commonly preferred
  4. Essential for integrating legacy code and third-party libraries
  5. Follows the Open/Closed Principle - extends functionality without modification