Design Patterns: Adapter

The Adapter design pattern resolves interface incompatibility issues by acting as a bridge between classes with disparate interfaces. It allows the integration of existing classes without modifying their source code, facilitating collaboration between incompatible components.

Design Patterns: Adapter

Problem

It's quite usual to encounter scenarios where existing classes or components have interfaces that are incompatible with each other. This interface mismatch can hinder collaboration between these components, making it challenging to integrate or reuse code. Modifying the existing code to conform to a new interface may not always be feasible, especially when dealing with third-party libraries or legacy systems.

Solution

The remedy for the problem is a structural design pattern named the Adapter. It provides a solution to the problem of incompatible interfaces by acting as a bridge between them. It allows classes with incompatible interfaces to work together without modifying their source code. The pattern achieves this by introducing a new class, known as the adapter, which translates the interface of one class into another that the client code expects. In essence, the Adapter pattern enables the integration of disparate components and promotes code reuse without requiring substantial modifications to the existing codebase.

There are two main types of adapters: Class Adapter and Object Adapter.

class adapter class diagram

The Class Adapter aims to adapt the interface of one class to match another. It achieves this by using multiple inheritance, where the adapter class inherits from both the existing class (adaptee) and the target interface. In this approach, the adapter class overrides or implements the methods of the target interface, while also inheriting the implementation of the adaptee.

class adapter sequence diagram

While the Class Adapter provides a solution to the interface incompatibility problem, its usage is limited in programming languages that do not support multiple inheritance. Consequently, this type of adapter is less prevalent in modern software development.

object adapter class diagram

The Object Adapter is another variant of the Adapter design pattern that addresses the issue of incompatible interfaces. In this approach, the adapter class contains an existing class (adaptee) instance and implements the target interface. Unlike the Class Adapter, the Object Adapter uses composition instead of inheritance to achieve adaptation.

object adapter sequence diagram

By encapsulating the adaptee within the adapter class and forwarding requests from the target interface methods to the corresponding methods of the adaptee, the Object Adapter provides a more flexible and widely applicable solution. This approach is particularly advantageous in languages that do not support multiple inheritance, making it a preferred choice in many scenarios.

Pros

  • Code Reusability: The adapter pattern allows the reuse of existing classes with incompatible interfaces, promoting code reusability without modifying their source code.
  • Interoperability: It enables the collaboration of components or classes with disparate interfaces, fostering interoperability between different parts of a system.
  • Flexibility: The pattern provides a flexible solution to interface mismatches, allowing integration without requiring significant changes to the existing codebase.
  • Maintainability: The adapter pattern enhances code maintainability by isolating the adaptation logic within the adapter classes, making it easier to manage and update.
  • Separation of Concerns: It separates the concerns of the existing class and the client code, promoting a clean separation of interfaces and implementations.

Cons

  • Complexity: Depending on the complexity of the adaptation logic, the adapter classes can introduce additional complexity to the system.
  • Runtime Overhead: Object Adapters, in particular, may introduce a slight runtime overhead due to the additional method calls required to forward requests from the target interface to the adaptee.
  • Potential for Overuse: Overusing the Adapter pattern without careful consideration can lead to a proliferation of adapter classes, which may complicate the overall design.
  • Limited Support in Some Languages: Class Adapters may face limitations in languages that do not support multiple inheritance, potentially restricting their applicability.
  • Design Overhead: In some cases, introducing adapters might be seen as an unnecessary design overhead, especially if the integration is a one-time occurrence or if the system undergoes frequent changes.

Implementation

Below are simple code snippets for the Adapter design pattern implemented in C++, Python, and Java programming languages.

Java

Class adapter

class ExternalLibrary {
    public String libraryMethod(String libraryData) {
        return "ExternalLibrary's specific request " + libraryData;
    }
}

interface ExistingClass {
    String someMethod(String data);
}

class LibraryAdapter extends ExternalLibrary implements ExistingClass {
    @Override
    public String someMethod(String data) {
        String convertedData = convertToLibraryFormat(data);
        String libraryResult = libraryMethod(convertedData);
        return convertFromLibraryFormat(libraryResult);
    }

    private String convertToLibraryFormat(String data) {
        return data;
    }

    private String convertFromLibraryFormat(String data) {
        return data;
    }
}

In the provided code, the class Adapter pattern is demonstrated where the ExternalLibrary class serves as the existing class (adaptee) with a method libraryMethod(String libraryData) representing its functionality. The ExistingClass interface defines a method someMethod(String data) expected by the client. The LibraryAdapter class acts as the adapter, extending ExternalLibrary and implementing ExistingClass, thereby bridging the gap between the external library and the client code. Its someMethod(String data) implementation internally calls libraryMethod(String libraryData) from ExternalLibrary, handling necessary data conversions using private methods convertToLibraryFormat(String data) and convertFromLibraryFormat(String data).

class ClientClass {

    ExistingClass existingClass;

    public ClientClass(ExistingClass existingClass) {
        this.existingClass = existingClass;
    }

    void execute() {
        existingClass.someMethod("test");
    }

    public static void main(String[] args) {
        ExistingClass existingClass = new LibraryAdapter();
        ClientClass clientClass = new ClientClass(existingClass);
        clientClass.execute();
    }
}

The ClientClass interacts with the adapter through the ExistingClass interface, enabling seamless integration of the external library functionality into the client code.

Object adapter

class ExternalLibrary {
    public String libraryMethod(String libraryData) {
        return "ExternalLibrary's specific request " + libraryData;
    }
}

interface ExistingClass {
    String someMethod(String data);
}

class LibraryAdapter implements ExistingClass {

    private ExternalLibrary adaptee;

    public LibraryAdapter(ExternalLibrary externalLibrary) {
        this.adaptee = externalLibrary;
    }

    @Override
    public String someMethod(String data) {
        String convertedData = convertToLibraryFormat(data);
        String libraryResult = adaptee.libraryMethod(convertedData);
        return convertFromLibraryFormat(libraryResult);
    }

    private String convertToLibraryFormat(String data) {
        return data;
    }

    private String convertFromLibraryFormat(String data) {
        return data;
    }
}

In the provided code, the object Adapter pattern is implemented. The ExternalLibrary class represents an existing external library with a method libraryMethod(String libraryData) that performs a specific functionality. The ExistingClass interface defines a method someMethod(String data) expected by the client code. The LibraryAdapter class acts as the adapter, implementing the ExistingClass interface and encapsulating an instance of ExternalLibrary as its adaptee. Within the LibraryAdapter, the someMethod(String data) implementation converts the input data to a format compatible with the external library using a private method convertToLibraryFormat(String data), invokes the libraryMethod(String libraryData) on the adaptee (ExternalLibrary), and then converts the result back to the format expected by the client using another private method convertFromLibraryFormat(String data).

class ClientClass {

    ExistingClass existingClass;

    public ClientClass(ExistingClass existingClass) {
        this.existingClass = existingClass;
    }

    void execute() {
        existingClass.someMethod("test");
    }
    
    public static void main(String[] args) {
        ExternalLibrary externalLibrary = new ExternalLibrary();
        ExistingClass existingClass = new LibraryAdapter(externalLibrary);
        ClientClass clientClass = new ClientClass(existingClass);
        clientClass.execute();
    }
}

This structure allows the ExternalLibrary to be used seamlessly within the client code that expects to interact with the ExistingClass interface, demonstrating the Adapter pattern's principle of adapting the interface of one class to another.

C++

Class adapter

class ExternalLibrary {
public:
  std::string libraryMethod(std::string libraryData) {
    return "ExternalLibrary's specific request " + libraryData + "\n";
  }
};

class ExistingClass {
public:
  virtual ~ExistingClass() = default;
  virtual std::string someMethod(std::string data) = 0;
};

class LibraryAdapter : public ExistingClass, public ExternalLibrary {
private:
  std::string convertToLibraryFormat(std::string data) { return data; }

  std::string convertFromLibraryFormat(std::string data) { return data; }

public:
  std::string someMethod(std::string data) override {
    std::string convertedData = convertToLibraryFormat(data);
    std::string libraryResult = libraryMethod(data);
    return convertFromLibraryFormat(libraryResult);
  }
};

The code defines the ExternalLibrary class representing an existing class with a method libraryMethod(std::string libraryData) for a specific functionality, while the ExistingClass class defines an interface with a virtual method someMethod(std::string data) expected by the client. The LibraryAdapter class serves as the adapter, strangely implementing both the ExistingClass interface and inheriting from the ExternalLibrary class. Within LibraryAdapter, the someMethod(std::string data) implementation performs data conversion and directly invokes libraryMethod(std::string libraryData) from ExternalLibrary, bypassing traditional composition or delegation. Though technically achieving adaptation, this approach introduces potential issues such as ambiguity and tight coupling, deviating from the usual emphasis on composition or single interface inheritance in the Adapter pattern.

class ClientClass {
public:
  ExistingClass* existingClass;

  ClientClass(ExistingClass* existingClass) { this->existingClass = existingClass; }

  void execute() { existingClass->someMethod("test"); }
};

The ClientClass serves as a client that interacts with an object implementing the ExistingClass interface. The constructor of ClientClass accepts an instance of ExistingClass as a parameter and stores it as a member variable. The execute() method of ClientClass simply calls the someMethod() function on the existingClass object, passing it the string "test".

ExistingClass* existingClass = new LibraryAdapter;
ClientClass clientClass(existingClass);
clientClass.execute();

An instance of LibraryAdapter, which acts as the adapter, is created and assigned to a pointer of type ExistingClass. The LibraryAdapter class likely adapts the functionality of another class (not shown in this snippet) to match the interface defined by ExistingClass. Then, an instance of ClientClass is created, passing the existingClass pointer to its constructor. Finally, the execute() method of ClientClass is invoked, which internally utilizes the someMethod() function provided by the existingClass. This code demonstrates how the Adapter pattern allows a client class (ClientClass) to seamlessly interact with an adapted class (LibraryAdapter) through a common interface (ExistingClass), without needing to know the specific implementation details of the adapted class.

Object adapter

class ExternalLibrary {
public:
  std::string libraryMethod(std::string libraryData) {
    return "ExternalLibrary's specific request " + libraryData + "\n";
  }
};

class ExistingClass {
public:
  virtual ~ExistingClass() = default;
  virtual std::string someMethod(std::string data) = 0;
};

class LibraryAdapter : public ExistingClass {
private:
  ExternalLibrary* adaptee;

  std::string convertToLibraryFormat(std::string data) { return data; }

  std::string convertFromLibraryFormat(std::string data) { return data; }

public:
  LibraryAdapter(ExternalLibrary* adaptee) : adaptee(adaptee) {}

  std::string someMethod(std::string data) override {
    std::string convertedData = convertToLibraryFormat(data);
    std::string libraryResult = adaptee->libraryMethod(data);
    return convertFromLibraryFormat(libraryResult);
  }
};

The ExternalLibrary class represents an existing class (adaptee) with a method libraryMethod(std::string libraryData) that performs a specific functionality. The ExistingClass class defines an interface with a virtual method someMethod(std::string data) that is expected by the client code. The LibraryAdapter class acts as the adapter, implementing the ExistingClass interface and encapsulating an instance of ExternalLibrary as its adaptee. Within the LibraryAdapter, the someMethod(std::string data) implementation converts the input data to a format compatible with the external library using private methods convertToLibraryFormat(std::string data) and convertFromLibraryFormat(std::string data), invokes the libraryMethod(std::string libraryData) on the adaptee (ExternalLibrary), and then converts the result back to the format expected by the client.

class ClientClass {
public:
  ExistingClass* existingClass;

  ClientClass(ExistingClass* existingClass) { this->existingClass = existingClass; }

  void execute() { existingClass->someMethod("test"); }
};

The ClientClass serves as a client that interacts with an object implementing the ExistingClass interface. It takes an instance of ExistingClass as a parameter in its constructor and stores it as a member variable. The execute() method of ClientClass simply calls the someMethod() function on the existingClass object, passing it the string "test".

    ExternalLibrary* externalLibrary = new ExternalLibrary;
    ExistingClass* existingClass = new LibraryAdapter(externalLibrary);
    ClientClass clientClass(existingClass);
    clientClass.execute();

    delete existingClass;
    delete externalLibrary;

This code demonstrates managing the interaction between an existing external library (ExternalLibrary) and a client class (ClientClass) through the use of an adapter (LibraryAdapter). Initially, an instance of ExternalLibrary is created. Then, an instance of LibraryAdapter is created, which adapts ExternalLibrary to the ExistingClass interface. This adapter instance is then passed as a parameter to instantiate the ClientClass. Finally, the execute() method of ClientClass is invoked, triggering the execution of the adapted method someMethod() through the adapter, thereby enabling the client to utilize the functionality provided by the external library.

Python

Class adapter

class ExternalLibrary:
    def library_method(self, data: str) -> str:
        return f'ExternalLibrary\'s specific request {data}'


class ExistingClass:
    def some_method(self, data: str) -> str:
        pass


class LibraryAdapter(ExternalLibrary, ExistingClass):
    def __convert_to_library_format(self, data: str) -> str:
         return data

    def __convert_from_library_format(self, data: str ) -> str:
        return data

    def some_method(self, data) -> str:
        converted_data = self.__convert_to_library_format(data);
        library_result = self.library_method(converted_data)
        return self.__convert_from_library_format(library_result);

The ExternalLibrary class represents an existing external library with a method library_method(data: str) -> str that performs a specific functionality. The ExistingClass class defines an interface with a method some_method(data: str) -> str expected by the client code. The LibraryAdapter class acts as the adapter, inheriting from both ExternalLibrary and ExistingClass. Within the LibraryAdapter, the some_method(data) implementation converts the input data to a format compatible with the external library using private methods __convert_to_library_format(data: str) -> str and __convert_from_library_format(data: str) -> str. It then calls the library_method(data) from ExternalLibrary, and finally converts the result back to the format expected by the client. This structure allows the ExternalLibrary to be used seamlessly within the client code that expects to interact with the ExistingClass interface.

class ClientClass:
    def __init__(self, existing_class: ExistingClass) -> None:
        self.__existing_class = existing_class

    def execute(self) -> None:
        self.__existing_class.some_method('test')

ClientClass serves as a client that interacts with an object implementing the ExistingClass interface. The constructor of ClientClass accepts an instance of ExistingClass as a parameter and stores it as a private member variable. The execute() method of ClientClass then simply calls the some_method() function on the existing_class object, passing it the string 'test'.

existing_class = LibraryAdapter()
client_class = ClientClass(existing_class)
client_class.execute()

Initially, an instance of LibraryAdapter is created, representing the adapter class that adapts the functionality of another class to match the interface defined by ExistingClass. Then, an instance of ClientClass is instantiated, passing the existing_class object (which is an instance of LibraryAdapter) to its constructor. Finally, the execute() method of ClientClass is called, internally utilizing the some_method() function provided by the existing_class. This arrangement demonstrates how the Adapter pattern enables a client class (ClientClass) to interact seamlessly with an adapted class (LibraryAdapter) through a common interface (ExistingClass), abstracting away the details of the adapted class's implementation from the client code.

Object adapter

class ExternalLibrary:
    def library_method(self, data: str) -> str:
        return f'ExternalLibrary\'s specific request {data}'


class ExistingClass:
    def some_method(self, data: str) -> str:
        pass


class LibraryAdapter(ExistingClass):
    def __init__(self, external_library: ExternalLibrary) -> None:
        self.__external_library = external_library

    def __convert_to_library_format(self, data: str) -> str:
         return data

    def __convert_from_library_format(self, data: str ) -> str:
        return data

    def some_method(self, data) -> str:
        converted_data = self.__convert_to_library_format(data);
        library_result = self.__external_library.library_method(converted_data)
        return self.__convert_from_library_format(library_result);

The ExternalLibrary class denotes an existing external library with a method library_method(data: str) -> str for specific functionality, while ExistingClass defines an interface with some_method(data: str) -> str expected by client code. The LibraryAdapter class acts as the adapter, inheriting from ExistingClass and encapsulating an instance of ExternalLibrary. Within LibraryAdapter, some_method(data) converts input data to a compatible format for the external library, invoking library_method(data) from the ExternalLibrary instance, and then converting the result back to the expected format. This structure facilitates seamless utilization of ExternalLibrary within client code expecting interaction with ExistingClass interface.

class ClientClass:
    def __init__(self, existing_class: ExistingClass) -> None:
        self.__existing_class = existing_class

    def execute(self) -> None:
        self.__existing_class.some_method('test')

The ClientClass serves as a client that interacts with an object implementing the ExistingClass interface. The constructor of ClientClass accepts an instance of ExistingClass as a parameter and stores it as a private member variable. The execute() method of ClientClass then simply calls the some_method() function on the existing_class object, passing it the string 'test'.

external_library = ExternalLibrary()
existing_class = LibraryAdapter(external_library)
client_class = ClientClass(existing_class)
client_class.execute()

Initially, an instance of ExternalLibrary is created, representing an external library with specific functionality. Then, an instance of LibraryAdapter is instantiated, passing the external_library object as a parameter. The LibraryAdapter acts as the adapter, adapting the functionality of ExternalLibrary to match the interface expected by ExistingClass. Subsequently, an instance of ClientClass is created, passing the existing_class object (which is an instance of LibraryAdapter) to its constructor. Finally, the execute() method of ClientClass is invoked, internally utilizing the some_method() function provided by the existing_class. This demonstrates how the Adapter pattern enables a client class (ClientClass) to interact seamlessly with an adapted class (LibraryAdapter) through a common interface (ExistingClass), abstracting away the details of the adapted class's implementation from the client code.

Summary

The Adapter design pattern serves as a mediator between classes with incompatible interfaces, allowing them to collaborate seamlessly without altering their source code. By providing both Class and Object Adapter variants, it offers flexibility in adapting interfaces through either inheritance or composition. Through this pattern, code reusability, interoperability, and maintainability are enhanced, making it a valuable tool for integrating disparate components in software development projects. However, careful consideration should be given to the potential complexity and overhead introduced by adapters, ensuring they are applied judiciously to achieve the desired system design goals.