Python patterns for fun and profit: The Adapter Pattern

Introduction

The Adapter pattern is used to make one interface compatible with another. It allows objects with different, and therefore incompatible interface to work together.

If you for example want to bring an existing class into your system with an interface that doesn’t match any existing interface, and you can not modify this class, you write an adapter class to bridge the gap.

It looks like this:

The pattern consists of different parts:

  • The client has an object which implements the Subject interface.
  • The object in question is the Adapter.
  • The implementation of the operation() method in the Adapter wraps the concrete_operation in the RealSubject class.

That way we can talk to the RealSubject class using an already known interface. The Adapter class takes care of converting our method-calls for the RealSubject. Also, because this pattern is loosely coupled, changing the RealSubject should be relatively painless.

What is the difference between the Decorator and the Adapter pattern?

DecoratorAdapter
PurposeThe decorator pattern is used to add behaviour to objects dynamically without modifying the source.Make one interface compatible with another
UseThis pattern is used to when you want to add or extend behaviour to existing classes in a flexible and reusable way without creating a hierarchy of subclasses.Used to integrate an existing class with an interface which is not compatible with existing interfaces. Modifying this class is not needed if you wrap it in an Adapter.
StructureCreating a decorator pattern involves creating a set of decorator classes that implement the same interface as the component, thereby extending or modifying its behaviourCreate an Adapter class, implementing the desired interface. This class also holds an instance of the class you want to adapted, the adaptee. The adapter delegates calls to the adaptee converting or transforming the input or output of these calls.
ExampleImagine having a bakery, you add things like GlazingDecorator or a WhippedCreamDecorator wrapping around your Pie-class to produces pies of the desired kindA classic example would be the conversion of units of measurement. If you have a classes which expects lengths to be in feet and inches, yet your client needs meters, you can wrap the classes in ConverterAdapter and have the conversion done automatically

Difference between Decorator and Adapter

In short, Adapter is used to make incompatible things work together, while the Decorator adds and extends functionality.

Implementation in Python

Preparing the stage

We will start by importing the following:

from abc import ABC, abstractmethod

Line by line:

  1. ABC stands for ‘Abstract Base Class’. When you derive from this class you simply tell Python that your class is meant to be abstract. This means that it is designed to be a template or a blueprint which should function as a baseclass, but should have no concrete implementation or instance of its own.
  2. The @abstractmethod decorator marks specific methods as abstract. This means that must and should be implemented by any concrete (i.e. non-abstract) subclass.

The target interface

We start by defining the interface of our target, that is the interface which our clients expect:

class Target(ABC):
    @abstractmethod
    def request(self) -> str:
        pass

Line by line:

  1. The Target interface derives from ABC so it is an abstract base class.
  2. It defines one abstract method called request().

The legacy service

To demonstrate the idea of an adapter, let’s define some LegacyService with a different interface:

class LegacyService:
    def specific_request(self, data: str) -> str:
        return f"LegacyService processing: {data}"

As you can see the interface of the LegacyService is different from our target.

The adapter

Since this interface is different, we need an adapter:

class ServiceAdapter(Target):
    def __init__(self, legacy_service: LegacyService):
        self._legacy_service = legacy_service
    
    def request(self) -> str:
        return self._legacy_service.specific_request("adapted data")

Line by line:

  1. The ServiceAdapter inherits from the Target interface, which means it must implement the request() method.
  2. The constructor receives an object of type LegacyService as its argument.
  3. The request() method basically redirects a call to request() to the specific_request() method on the LegacyService

The client code

All we now need is a small function to call our request() method:

def client_code(target: Target):
    return target.request()

Time to test

We can now test our adapter:

if __name__ == "__main__":
    legacy = LegacyService()
    adapter = ServiceAdapter(legacy)
    print(client_code(adapter))

What happens here is:

  1. We create our legacy service.
  2. We wrap it in our adapter,
  3. And call the desired functionality.

Conclusion

In conclusion, the Adapter pattern stands out as an indispensable tool in the object-oriented design toolkit, primarily serving as a vital bridge for resolving interface incompatibilities. Its function is elegantly simple yet profoundly impactful: enabling existing, potentially unmodifiable classes (the adaptee, or LegacyService in our example) to fulfill the contract of a client’s expected interface (the Target).

The Python implementation effectively showcased the mechanics of this process, utilizing inheritance to conform to the Target and delegation to translate the method call for the LegacyService. This deliberate loose coupling is its greatest strength, promoting extensive code reuse and simplifying the integration of external dependencies or complex legacy systems without requiring disruptive internal modifications. The Adapter shields the client from the underlying complexity of the original service, resulting in code that is more stable and maintainable over time.

Furthermore, understanding the Adapter pattern is incomplete without clearly distinguishing it from the Decorator pattern. While the Decorator is dedicated to the dynamic addition or extension of responsibilities, the Adapter’s mission is purely one of transformation and interoperability. By encapsulating the conversion logic within a dedicated adapter class, we ensure that the client code remains clean and ignorant of the interface friction beneath it. This strategic isolation solidifies the Adapter pattern’s status as a cornerstone of flexible and resilient software architecture.

The Code Nomad
The Code Nomad
Articles: 167

Leave a Reply

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