Structural Patterns
Adapter
Convert the interface of a class into another interface clients expect, allowing incompatible classes to work together
The Adapter Pattern (also known as the Wrapper Pattern) is a structural design pattern that acts as a translator between two incompatible interfaces. It wraps an existing class with a new interface matching the client's expectations, enabling integration with third-party libraries, legacy codebases, and vendor APIs without modifying their source code.
1. Learning Objectives
- Identify when to apply the Adapter Pattern to link incompatible software components.
- Compare the tradeoffs of the Object Adapter (Composition) and Class Adapter (Multiple Inheritance) patterns.
- Construct adapter wrapper classes mapping different method names and parameter types.
- Analyze compile-time dependencies when wrapping third-party libraries.
- Implement payment integration adapters in Java, Python, and C++.
2. Problem & Naive Solution
Suppose you are building a checkout system. The application defines a standard PaymentProcessor interface that handles transactions using card details and amounts.
To expand globally, you must integrate a third-party gateway, StripeSDK. However, the SDK's payment method is incompatible: it uses tokenized authorization payloads and different parameter orders.
The Naive Solution
A developer might modify the checkout system directly, bypassing the abstraction to call the SDK methods:
This approach has significant flaws:
- Violates Open/Closed Principle: Adding another provider (e.g. PayPal) forces us to rewrite the client service logic.
- Source Code Inaccessibility: We cannot rewrite the third-party
StripeSDKclass to implement ourPaymentProcessorinterface because it belongs to an external vendor. - Untestability: The service is tightly coupled to the external SDK class, preventing the injection of mock objects for unit testing.
3. Issues
Direct integration of external libraries creates tight coupling. Any update to the third-party library's API signatures forces changes across multiple client modules, causing compile-time errors and deployment delays.
4. Pattern Introduction & UML
The Adapter Pattern decouples the client from incompatible classes by introducing a translator object:
- Target (
PaymentProcessor): The interface the client expects. - Adapter (
StripeAdapter): Implements the Target interface, holds a reference to the Adaptee, and translates calls. - Adaptee (
StripeSDK): The incompatible legacy or third-party class that provides the required functionality. - Client (
CheckoutService): Interacts with the system purely through the Target interface.
UML: Object Adapter (Composition-based)
5. Participants
- Target (
PaymentProcessor): The abstract interface defining the domain contract. - Adaptee (
StripeSDK): The third-party API containing the necessary business operations. - Adapter (
StripeAdapter): The wrapper class that maps target methods to the adaptee API. - Client (
CheckoutService): Uses the target interface to execute transactions.
6. Theory (Object Adapter vs. Class Adapter)
There are two ways to implement the Adapter pattern:
- Object Adapter (Composition): The adapter implements the target interface and holds a private instance of the adaptee (uses composition). It delegates method calls to the adaptee, translating arguments. This is the preferred approach as it works in all languages and supports polymorphic subclass wrapping.
- Class Adapter (Inheritance): The adapter inherits from both the target interface and the concrete adaptee class (uses multiple inheritance). It overrides the target's methods and calls the inherited adaptee methods directly. This approach is limited to languages that support multiple class inheritance (e.g. C++).
7. Syntax Explanation
Implementing the pattern requires wrapping the adaptee reference in the adapter's field variables:
- Java: Uses composition (e.g.,
private final StripeSDK sdk;) to hold the adaptee instance inside the adapter class. - Python: Leverages dynamic delegation, allowing the adapter to forward method calls dynamically.
- C++: Uses smart pointers (e.g.
std::unique_ptr<StripeSDK>) to wrap the adaptee instance safely, or uses multiple inheritance to implement a Class Adapter.
8. Step-by-Step Implementation
- Step 1: Define the target domain interface that the client expects (e.g.,
PaymentProcessor). - Step 2: Declare the concrete adapter class implementing the target interface.
- Step 3: Inject the incompatible adaptee class instance (e.g.,
StripeSDK) into the adapter constructor, saving it as a private field. - Step 4: Inside the adapter methods, translate parameters (e.g., extracting token details) and delegate execution to the private adaptee reference.
- Step 5: Modify the client class constructor to accept the target interface, shielding it from concrete adapter details.
9. Complete Code (Mini Project)
10. Code Walkthrough
Let's review the integration logic:
- The client class
CheckoutServicedepends strictly on the target interfacePaymentProcessor. It has no compiled dependency or knowledge of Stripe or PayPal classes. - The
StripeAdapterimplements thePaymentProcessorinterface. It serves as the middleman, wrapping the concreteStripeSDK. - When the client calls
processPayment(), the adapter translates parameters (creating transaction tokens from emails) and delegates execution by callingchargeTokenized()on the wrapped SDK.
11. Execution Flow
- Initialization: The application instantiates the third-party
StripeSDK. It then instantiatesStripeAdapter, injecting the SDK reference. - Client Trigger: The client triggers the checkout process (
checkout.purchaseCart(...)). - Translation: The client calls
processPayment()on the adapter. The adapter intercepts the call, generates the required tokens, and aligns parameters. - Delegation: The adapter calls
chargeTokenized()on the wrapped SDK instance, executing the payment.
12. Internal Working (Call Delegation Stack)
Method translation and forwarding add a small execution stack overhead:
- Stack Frame Allocation: When the client calls
processPayment(), a stack frame is allocated. The adapter then callschargeTokenized(), pushing another stack frame. - Garbage Collection: Temporary objects created during parameter translation (like temporary token strings) are allocated on the heap and GC-collected after the method returns, adding minor memory overhead.
13. Complexity Analysis
- Time Complexity: $O(1)$ constant time overhead for translating parameters and delegating method calls.
- Space Complexity: $O(1)$ constant space. Wrapping requires storing a single reference to the adaptee on the heap.
14. Best Practices
- Prefer Object Adapter (Composition): Favor composition over inheritance. Wrapping the adaptee instance is cleaner, works in single-inheritance languages, and allows the adapter to wrap any adaptee subclass.
- Keep Adapters Cohesive: Keep translation logic simple. Avoid adding unrelated business logic or validation checks inside the adapter class.
- Program to Interfaces: Ensure the client depends only on the target interface, keeping it decoupled from concrete adapter implementations.
15. Common Mistakes
- Forgetting to translate exceptions: Allowing the third-party adaptee's database or network exceptions to propagate raw to the client. The adapter should catch and wrap these in domain-specific exceptions.
- Confusing Adapter with Facade: Using an adapter to simplify a complex subsystem interface. An adapter changes an interface to make it compatible; a Facade simplifies an interface without changing its protocol.
- Over-adapting: Wrapping classes when simple method signature updates or refactoring would resolve the incompatibility.
16. Framework Usage
- Java's Arrays.asList(): Adapts a standard array into a List collection (e.g.
List<String> list = Arrays.asList(array);). - Spring MVC's HandlerAdapter: Spring MVC uses handler adapters to support different controller execution strategies (e.g. SimpleController, AnnotationController) without modifying the dispatcher servlet.
17. Interview Discussion
Answer: - Adapter translates an incompatible interface to match client expectations. - Facade simplifies a complex subsystem interface without changing its protocol. - Bridge separates abstraction from implementation, allowing them to vary independently.
Answer: A Class Adapter uses multiple inheritance (inheriting from both the Target interface and the concrete Adaptee class). Since Java does not support multiple class inheritance, Object Adapter (composition) is preferred.
Answer: A Two-Way Adapter implements both the Target interface and the Adaptee interface. This allows the adapter to act as a Target for new clients and as an Adaptee for legacy systems simultaneously.
18. Practice Exercises
- Easy: Write an adapter in Python to make a
ThirdPartyTemperatureSensor(exposing temperature in Fahrenheit) work with a system expecting Celsius. - Medium: Create a C++
LoggerAdapterthat wraps a legacy log utility (which usesprintfsignatures) to match a modern structured logger interface. - Hard: Design an adapter system in Java that wraps a third-party XML parser library to implement a standard
JSONConfigurationParserinterface, including parsing translations.
19. Challenge Problem
Design an Analytics Dashboard Integrator. The application imports client logs containing user events in JSON format. You must integrate a legacy enterprise system that exports events in XML format, and a third-party tracking tool that exposes events via CSV format. Design an adapter-based architecture so the central dashboard can ingest events from all three sources using a unified interface. Write the implementation code in Java, Python, or C++ and verify that the dashboard can process events polymorphically.
20. Summary & Cheat Sheet
- The Adapter pattern translates incompatible interfaces to allow classes to work together.
- Object Adapter uses composition (preferred); Class Adapter uses inheritance.
- Adapters should only handle interface translation and parameter alignment, not business logic.
- Target interfaces shield clients from concrete adapter details.
21. Quiz
1. What is the primary purpose of the Adapter pattern?
A) To simplify a complex subsystem interface
B) To translate an incompatible interface into an interface clients expect (Correct)
C) To decouple abstractions from implementations
2. What is the alternative name for the Adapter pattern?
A) Bridge
B) Proxy
C) Wrapper (Correct)
3. Why is Object Adapter preferred over Class Adapter?
A) It compiles faster
B) It uses composition, making it compatible with single-inheritance languages and supporting polymorphic subclass wrapping (Correct)
C) It avoids virtual dispatch overhead
4. In Class Adapter, which mechanism is used to link target and adaptee?
A) Object Composition
B) Multiple Class Inheritance (Correct)
C) Static helper methods
5. Which java framework method adapts a standard array into a List collection?
A) Arrays.asList() (Correct)
B) Collections.singleton()
C) List.copyOf()
6. What should the adapter do if the wrapped library throws vendor-specific exceptions?
A) Propagate the exceptions directly to the client
B) Catch and wrap them in domain-specific exceptions (Correct)
C) Suppress the exceptions entirely
7. How does the Adapter pattern differ from the Facade pattern?
A) Adapter changes an interface to resolve incompatibility; Facade simplifies an interface without changing its protocol (Correct)
B) Adapter uses inheritance; Facade uses composition
C) There is no difference between them
8. Can an adapter wrap multiple adaptee instances?
A) Yes, if it needs to translate a target operation by delegating to multiple helper classes (Correct)
B) No, an adapter can wrap only a single class
C) Only if they share the same package
9. In C++, how are memory allocations typically managed in Object Adapters?
A) Via global variables
B) Using smart pointers (e.g. std::unique_ptr) to wrap the adaptee instance (Correct)
C) Using raw heap pointers without destructors
10. What is a Two-Way Adapter?
A) An adapter that can translate between two different data formats (e.g., XML and JSON)
B) An adapter that implements both the Target and Adaptee interfaces, supporting both legacy and new clients (Correct)
C) An adapter that operates on two threads simultaneously
22. Next Lesson Preview
In the next lesson, we will cover the Facade Pattern. We will learn how to provide a simplified, unified interface to a complex subsystem of classes!
Related Topics
- FacadeProvide a unified, simplified interface to a set of interfaces in a subsystem, making the subsystem easier to use and decoupling clients.
- DecoratorAttach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
- CompositeCompose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.