Adapter pattern

Adapter

Also known as: Wrapper

 Intent

The Adapter pattern is a structural design pattern that enables collaboration between objects with incompatible interfaces.

Adapter design pattern

 Problem

Picture this: You’re developing a stock market monitoring application. It retrieves stock data from various sources in XML format and presents visually appealing charts and diagrams to the user.

As you aim to enhance the application, you consider integrating a sophisticated third-party analytics library. However, there’s a hurdle: the analytics library exclusively operates with data in JSON format.

The structure of the app before integration with the analytics library
You can’t use the analytics library “as is” because it expects the data in a format that’s incompatible with your app.

Adapting the library to handle XML data is a potential solution, but it carries the risk of disrupting existing code dependent on the library. Furthermore, modifying the library might not be feasible if its source code is inaccessible.

 Solution

You can develop an adapter, a specialized object designed to translate the interface of one object so that another object can effectively interact with it.

An adapter encapsulates one of the objects to handle the intricacies of conversion transparently. The encapsulated object remains oblivious to the presence of the adapter. For instance, you could envelop an object operating in metric units with an adapter that converts all measurements to imperial units like feet and miles.

Adapters not only facilitate data conversion into diverse formats but also facilitate collaboration between objects with differing interfaces. Here’s how it operates:

  1. The adapter acquires an interface compatible with one of the existing objects.
  2. Utilizing this interface, the existing object can securely invoke the adapter’s methods.
  3. Upon receiving a request, the adapter forwards it to the second object, conforming to the format and sequence expected by the second object.

In some cases, it’s feasible to develop a two-way adapter capable of translating calls in both directions.

Adapter's solution

Returning to our stock market application, to address the challenge of incompatible data formats, you can develop XML-to-JSON adapters for each class within the analytics library that your code directly interacts with. Subsequently, you modify your code to exclusively communicate with the library through these adapters. When an adapter receives a request, it converts the incoming XML data into a JSON format and forwards the call to the relevant methods of a wrapped analytics object.

 Real-World Analogy

The Adapter pattern example
A suitcase before and after a trip abroad.

When you embark on your first trip from the US to Europe, you might encounter an unexpected issue when attempting to charge your laptop. The electrical plug and socket standards vary between countries, meaning your US plug won’t be compatible with a German socket. To resolve this dilemma, you can employ a power plug adapter equipped with an American-style socket and a European-style plug, allowing you to seamlessly connect your devices despite the differing standards.

 Structure

Object adapter

This approach exemplifies the principle of object composition: the adapter effectively implements the interface of one object while encapsulating the other. It’s a versatile solution applicable across various programming languages.

Structure of the Adapter design pattern (the object adapter)
  1. The Client class embodies the existing program’s core functionality.
  2. The Client Interface defines a standard protocol that other classes must adhere to in order to collaborate seamlessly with the client code.
  3. The Service class represents a valuable entity, often a third-party or legacy component. Direct usage by the client is not feasible due to its incompatible interface.
  4. The Adapter class serves as a bridge between the client and the service: it implements the client interface while encapsulating the service object. Through the adapter, the client communicates with the service by making calls via the client interface, which are then translated into a format understandable by the wrapped service object.
  5. By interacting with the adapter solely through the client interface, the client code remains decoupled from the specific adapter implementation. Consequently, you can introduce new adapter types into the program without disrupting existing client code. This flexibility proves invaluable, particularly when the service class interface undergoes changes or replacements: a new adapter class can be seamlessly integrated without necessitating alterations to the client code.

Class adapter

This implementation utilizes inheritance, where the adapter inherits interfaces from both objects concurrently. It’s important to note that this approach is feasible only in programming languages supporting multiple inheritance, like C++.

Adapter design pattern (class adapter)
  1. In the Class Adapter pattern, there’s no necessity to encapsulate objects, as it inherits behaviors directly from both the client and the service. The adaptation occurs within the overridden methods of the adapter class. Consequently, the resultant adapter class can seamlessly replace an existing client class, providing compatibility between the client and the service without requiring additional wrapping of objects.

 Pseudocode

This example of the Adapter pattern is based on the classic conflict between square pegs and round holes.

Structure of the Adapter pattern example
Adapting square pegs to round holes.

The Adapter pretends to be a round peg, with a radius equal to a half of the square’s diameter (in other words, the radius of the smallest circle that can accommodate the square peg).

// Say you have two classes with compatible interfaces:
// RoundHole and RoundPeg.
class RoundHole is
    constructor RoundHole(radius) { ... }

    method getRadius() is
        // Return the radius of the hole.

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

class RoundPeg is
    constructor RoundPeg(radius) { ... }

    method getRadius() is
        // Return the radius of the peg.


// But there's an incompatible class: SquarePeg.
class SquarePeg is
    constructor SquarePeg(width) { ... }

    method getWidth() is
        // Return the square peg width.


// An adapter class lets you fit square pegs into round holes.
// It extends the RoundPeg class to let the adapter objects act
// as round pegs.
class SquarePegAdapter extends RoundPeg is
    // In reality, the adapter contains an instance of the
    // SquarePeg class.
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // The adapter pretends that it's a round peg with a
        // radius that could fit the square peg that the adapter
        // actually wraps.
        return peg.getWidth() * Math.sqrt(2) / 2


// Somewhere in client code.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // this won't compile (incompatible types)

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

 Applicability

The Adapter pattern is ideal in situations where you encounter an existing class with an incompatible interface relative to the rest of your codebase. It offers a means to seamlessly integrate this class by creating an intermediary class—a translator—between your code and the legacy, third-party, or otherwise peculiar class.

Furthermore, the pattern proves beneficial when you aim to reuse multiple existing subclasses lacking some shared functionality that cannot be added to the superclass. While one could extend each subclass and incorporate the missing functionality into new child classes, this approach often leads to code duplication across these classes, which is less than ideal.

A more elegant solution involves creating an adapter class to encapsulate the missing functionality. By wrapping objects lacking the desired features within the adapter, you dynamically gain access to the required functionality. However, for this strategy to succeed, the target classes must adhere to a common interface, and the adapter’s field should align with this interface. This approach ensures a more streamlined and maintainable codebase compared to extending subclasses and duplicating code.

 How to Implement

Imagine you have a legacy service class, perhaps from a third-party source, with an interface that doesn’t quite align with your needs. You also have one or more client classes in your application that could greatly benefit from utilizing this service, but their interfaces are incompatible.

To bridge this gap, you’ll declare a client interface that outlines the methods clients can use to interact with the service. This interface serves as a contract defining how clients communicate with the service.

Next, you’ll create an adapter class that implements the client interface. This adapter will act as an intermediary, translating requests from the client into a format that the service can understand. To facilitate this translation, the adapter class will contain a field to store a reference to the service object. This reference can be initialized via the constructor or passed when calling adapter methods, depending on your preferences.

With the adapter in place, you’ll begin implementing the methods of the client interface. Within these method implementations, the adapter will delegate the majority of the real work to the service object, intervening only as necessary to handle interface or data format conversions.

It’s crucial that clients interact with the adapter solely through the client interface. This ensures that any changes or extensions made to the adapters won’t impact the client code, fostering flexibility and maintainability in your application architecture.

 Pros, Cons

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

 Open/Closed Principle. You can introduce new types of adapters into the program without breaking the existing client code, as long as they work with the adapters through the client interface.

 The overall complexity of the code increases because you need to introduce a set of new interfaces and classes. Sometimes it’s simpler just to change the service class so that it matches the rest of your code.

Trinh Dinh Phuong

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *