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.
The pattern
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.
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.
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.
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.
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.