TUTORIAL SERIES

Design Patterns in Python: Adapter

Collaboration of Incompatible Objects

Amir Lavasani
7 min readSep 23, 2023

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Adapter Pattern

What is the Adapter Pattern?

The Adapter pattern is a structural design pattern that facilitates the interaction between two interfaces that are incompatible or cannot work together directly. It acts as a bridge, allowing objects with different interfaces to collaborate.

The primary goal of the Adapter pattern is to ensure that client code can work with classes it was not initially designed to support. It achieves this without altering the source code of either the client or the adaptee (the class with the incompatible interface).

Solving a Common Problem

Imagine you have a legacy component in your software that provides critical functionality. This component has an interface incompatible with the rest of your system, making it challenging to integrate smoothly. This is where the Adapter pattern comes to the rescue.

An adapter enabling the collaboration of incompatible objects
Dall-E generated image with the following concept: An adapter enabling the collaboration of incompatible objects

Key Components of the Adapter Pattern

To understand the Adapter pattern fully, let’s break down its key components:

  1. Target Interface: The target interface defines the contract that the client code expects. It is the interface the adapter will conform to, allowing the client to interact with the adaptee seamlessly.
  2. Adaptee: The adaptee is the class or component with an incompatible interface. It’s the object you want to make use of but cannot interact with directly from the client. Usually a 3rd party or legacy service.
  3. Adapter: The intermediary that bridges the gap between the target interface and the adaptee. It translates calls from the client in a way that the adaptee can understand and respond to.

There are two main implementation types: Object Adapter and Class Adapter.

Object Adapter

Object Adapter takes on the interface of one object while wrapping the behavior of another. You can use this approach in many popular programming languages.

Image from refactoring.guru
  1. Client: Contains program logic.
  2. Client Interface: Defines collaboration rules.
  3. Service: A useful, but incompatible, class.
  4. Adapter: Bridges client and service, translating calls.
  5. Decoupling: Client code remains adaptable without changes.

Class Adapter

Class Adapter, on the other hand, is a bit more specific. It relies on inheritance, inheriting interfaces from both objects at once. However, it’s only an option in programming languages that support multiple inheritance, like C++.

Image from refactoring.guru

In this implementation, there’s no need to wrap objects because it inherits functionalities from both the client and the service. The adaptation occurs through method overrides, creating an adapter that seamlessly substitutes for an existing client class.

Implementing the Adapter Pattern in Python

We’ll implement both Class Adapter and Object Adapter versions in Python to illustrate how the Adapter Pattern resolves interface incompatibilities in different ways.

Object Adapter Implementation

Suppose we have a legacy class called OldSystem with an incompatible interface that we want to use in our modern system. The OldSystem class has a method legacy_operation() that we need to adapt to work with our new codebase.

Here’s how we can create an object adapter in Python:

class OldSystem:
def legacy_operation(self):
return "Legacy operation"

class Adapter:
def __init__(self, old_system):
self.old_system = old_system

def new_operation(self):
return f"Adapter: {self.old_system.legacy_operation()}"

# Client code
def client_code(adapter):
result = adapter.new_operation()
print(result)

if __name__ == "__main__":
old_system = OldSystem()
adapter = Adapter(old_system)
client_code(adapter)

In this example, we create an Adapter class that wraps the OldSystem object and provides a new method, new_operation(), which conforms to our target interface. The client_code function demonstrates how the adapter allows the client to work seamlessly with the legacy OldSystem class.

Class Adapter Implementation

While the object adapter is a common implementation, the class adapter offers an alternative approach. In this case, we use multiple inheritance to achieve the adapter behavior.

Let’s adapt our previous example to use the class adapter approach:

class OldSystem:
def legacy_operation(self):
return "Legacy operation"

class Adapter(OldSystem):
def new_operation(self):
return f"Adapter: {self.legacy_operation()}"

# Client code
def client_code(adapter):
result = adapter.new_operation()
print(result)

if __name__ == "__main__":
adapter = Adapter()
client_code(adapter)

In this version, the Adapter class inherits from both the OldSystem class and the target interface. This allows it to override the legacy_operation() method to provide the adapted behavior. The client_code function remains the same.

Both object and class adapters have their advantages and trade-offs, and the choice between them depends on the specific requirements and constraints of your project.

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

An AI experiencing human emotions for the first time
Dall-E generated image with the following concept: An AI experiencing human emotions for the first time

Real-world Use Cases: Adapter in Action

The Adapter pattern is a powerful design pattern with various practical applications. Here are some common use cases:

  1. Legacy System Integration: When you need to integrate a legacy system or library with modern code, the Adapter pattern can make the transition smoother. It allows you to wrap the legacy code with an adapter, ensuring it conforms to the expected interface of the new system.
  2. Third-Party Libraries: When working with third-party libraries or APIs that do not align with your system’s requirements, adapters can serve as intermediaries. They translate the third-party interface into one that your codebase understands.
  3. Interface Evolution: As your software evolves, you may encounter situations where the interfaces of existing classes need to change. Adapters can help maintain backward compatibility by presenting the old interface while internally implementing the new one.

Favor Object Composition over Class Inheritance

The Adapter pattern’s flexibility makes it a valuable asset in scenarios where interface compatibility is essential for a system’s overall functionality.

Advantages of the Adapter Pattern

The Adapter pattern offers several advantages when applied judiciously:

  1. Code Reusability: Adapters facilitate code reuse by integrating existing classes without altering their source, reducing redundancy and enabling well-tested code in new contexts.
  2. Principle of Single Responsibility: The Adapter pattern aligns with the principle of single responsibility, ensuring that each class has a clear and specific role, thus improving code organization and maintainability.
  3. Open/Closed Principle: By adapting existing interfaces rather than modifying them directly, the Adapter pattern adheres to the open/closed principle, allowing for extension without altering existing code, enhancing system stability.
  4. Integration of Third-Party Code: When using external libraries or APIs, the Adapter pattern eases their integration, shielding your codebase from external changes and minimizing disruptions during updates.

Considerations and Potential Drawbacks

  1. Complexity: Adapters can add complexity, especially with many to manage, impacting code maintainability.
  2. Performance Overhead: The added layer may introduce slight performance overhead, a concern for high-performance applications.
  3. Design Implications: Adapter use may signal original design issues; consider comprehensive refactoring for core interface incompatibilities.
Collaboration of two incompatible systems through an adapter
Dall-E generated image with the following concept: Collaboration of two incompatible systems through an adapter

Relations with Other Patterns — TL;DR;

In the world of design patterns, various patterns often complement or relate to each other in solving distinct but interconnected design problems. Let’s see how the Adapter Pattern relates to other similar patterns: Bridge, Decorator, Proxy, and Facade.

Adapter vs. Bridge Pattern

While both Adapter and Bridge Patterns involve separating abstractions from implementations, their goals diverge.

  • Adapter: Ensures interface compatibility between classes with incompatible interfaces.
  • Bridge: Separates abstraction from implementation to enable independent variations.

Adapter vs. Decorator Pattern

The Adapter Pattern and the Decorator Pattern share similarities in their structural aspects. Both patterns involve adding a layer of functionality around an existing object. However, their intentions and applications differ significantly.

  • Adapter: Ensures interface compatibility through adaptation and translation.
  • Decorator: Dynamically adds responsibilities to objects without interface changes, often extending functionality at runtime.

Adapter vs. Proxy Pattern

The Adapter Pattern and the Proxy Pattern both act as intermediaries between a client and an object. However, their purposes and implementations vary significantly.

  • Adapter: Translates calls to ensure compatibility between interfaces.
  • Proxy: Controls object access, serving as a protective barrier with a focus on access control and lazy loading.

Adapter vs. Facade Pattern

Adapter and Facade Patterns share the goal of simplifying interactions with complex systems but differ in their approaches:

  • Adapter: Ensures compatibility between existing interfaces without altering source code.
  • Facade: Simplifies client interactions by providing a unified, higher-level interface to a group of interfaces, focusing on a streamlined experience rather than individual conversions.

In summary, while these patterns share similarities in terms of mediating interactions between clients and objects, they each have distinct purposes and are best suited for different scenarios. Understanding when and how to apply these patterns is crucial for effective software design.

Conclusion

The Adapter design pattern is a powerful tool in a software engineer’s toolkit, especially when dealing with legacy systems, third-party libraries, or evolving interfaces. It allows for the integration of incompatible components while promoting code reusability, flexibility, and maintainability.

As you embark on your software engineering journey, remember that design patterns, like the Adapter pattern, provide elegant solutions to common problems. By understanding and applying these patterns, you can create robust and adaptable software systems that stand the test of time.

Hope you enjoyed the Adapter pattern exploration 🙌 Happy coding! 👨‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Adapter Pattern
  3. The Composition Over Inheritance Principle
  4. Head First Design Patterns (Book)
  5. Adapter Method — Python Design Patterns
  6. Adapter Design Pattern

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃