Adapter Design Pattern in Python

Introduction

The Adapter Design Pattern is a popular Structural Design Pattern used in software engineering. This guide looks at how we can implement the Adapter Design Pattern in Python.

Design Patterns are template-like solutions – practically recipes for solving recurring, common problems in software development. The Adapter Pattern is based upon the concept of a real-world adapter! For instance, a laptop’s charger may have a 3-pin plug at the end, but the wall socket may only be a 2-pin socket. To plug a 3-pin charger into this socket, we’d need an adapter, that accepts a 3-pin plug, and adapts the interface into the 2-pin socket.

A 2-pin charger and a 3-pin charger have the same basic function (conduct electricity from the socket to the laptop), but have a different form, and one can easily adapt into the other. Whenever you have software components with the same basic function but different forms, you can apply the Adapter Design Pattern.

The Adapter Pattern follows this exact principle. It allows two incompatible interfaces to work together without modifying the internals of each component. This is achieved by adapting one interface, to another, externally.

Let’s look at some basic terminology before diving deeper into the world of Adapter Patterns:

  • Client Interface: An interface that specifies the functions that the client should implement.
  • Client: A class that implements the client interface.
  • Adaptee/Service: The incompatible class that needs to collaborate with the client interface.
  • Adapter: The class that makes the collaboration between the service and the client possible.

Different Types of Adapter Patterns

The adapter design pattern can be implemented in two different ways:

Object Adapter

With this method, the adapter class implements the methods from the client interface. Thus, the client object and the adapter object are compatible with each other. The service object forms a has-a relationship with the adapter object i.e. the service object belongs to the adapter object.

We know that the service class is not compatible with the client. The adapter class wraps around the service object by instantiating itself with that object. Now, the service object can be accessed through the adapter object, allowing the client to interact with it.

We can implement the object adapter in all of the modern programming languages.

Class Adapter

With this method, the adapter has an is-a relationship with the service class. In this scenario, the adapter implements the methods required by the client, but it inherits from multiple adaptees, giving it the ability to call their incompatible functions directly. The biggest drawback with this variation is that we can only use it in the programming languages that support multiple inheritance of classes.

Implementation of the Adapter Design Pattern in Python

In the section below, we will implement the Adapter design pattern in Python, specifically using the object adapter variation. The section is divided into two parts. First, we will create the environment where the Adapter Pattern should be used. It’s important to see clearly how this pattern can solve some software problems. The second section will use an adapter to resolve the issue.

Incompatibility Issue Between Classes

Let’s look at the compatibility issue when the client and service class implement different functionalities. Create a client class with the following methods and save it in a folder as car.py:

import random

class Car:
    def __init__(self):
        self.generator = random.Random()

    def accelerate(self):
        random_num = self.generator.randint(50, 100)
        speed = random_num
        print(f"The speed of the car is {speed} mph")

    def apply_brakes(self):
        random_num = self.generator.randint(20, 40)
        speed = random_num
        print(f"The speed of the car is {speed} mph after applying the brakes")

    def assign_driver(self, driver_name):
        print(f"{driver_name} is driving the car")

Here, we have created a Car class with three methods accelerate(), apply_brakes() and assign_driver(). We imported the random module and used it to generate numbers that set the car’s speed after accelerating and applying the brakes. The assign_driver() method displays the car driver’s name.

Next, we have to create a service or adaptee class that wishes to collaborate with the client class Car. Create a Motorcycle class like this and save it in your folder as motorcycle.py:

import random

class Motorcycle:
    def __init__(self):
        self.generator = random.Random()

    def rev_throttle(self):
        random_num = self.generator.randint(50, 100)
        speed = random_num
        print(f"The speed of the motorcycle is {speed} mph")

    def pull_brake_lever(self):
        random_num = self.generator.randint(20, 40)
        speed = random_num
        print(
            f"The speed of the motorcycle is {speed} mph after applying the brakes")

    def assign_rider(self, rider_name):
        print(f"{rider_name} is riding the motorcycle")  

A service class, Motorcycle is created above with three methods rev_throttle(), pull_brake_lever(), and assign_rider(). Notice the difference between the methods of the service and client class despite their similar functionality. The accelerator() method increases the speed of the car while the rev_throttle() method increases the motorcycle’s speed. Likewise, apply_brakes() and pull_brake_lever() applies brakes in the respective vehicles. Finally, the assign_driver() and assign_rider() methods assign the vehicle operator.

Next, let’s create a class to access these different methods. First, add an __init.py__ in the same folder you created car.py and motorcycle.py:

touch __init__.py

Now add the following code in a new file drive.py:

from car import Car
from motorcycle import Motorcycle
import traceback

if __name__ == '__main__':
    car = Car()
    bike = Motorcycle()

    print("The Motorcyclen")
    bike.assign_rider("Subodh")
    bike.rev_throttle()
    bike.pull_brake_lever()
    print("n")

    print("The Carn")
    car.assign_driver("Sushant")
    car.accelerate()
    car.apply_brakes()
    print("n")

    print("Attempting to call client methods with the service objectn")

    try:
        bike.assign_driver("Robert")
        bike.accelerate()
        bike.apply_brakes()
    except AttributeError:
        print("Oops! bike object cannot access car methods")
        traceback.print_exc()

This script that creates our client and service objects. We first import the Car and Motorcycle classes and create objects with them. Then we invoke the methods from the bike object (Motorcycle class). After, we invoke the methods of the car object (Car class). When executed, all the code mentioned so far will work.

However, an exception is raised when we try to invoke the methods of the Car class with the bike object. When we run this script:

The Motorcycle

Subodh is riding the motorcycle
The speed of the motorcycle is 91 mph
The speed of the motorcycle is 37 mph after applying the brakes


The Car

Sushant is driving the car
The speed of the car is 59 mph
The speed of the car is 33 mph after applying the brakes


Attempting to call client methods with the service object

Oops! bike object cannot access car methods
Traceback (most recent call last):
  File "drive.py", line 24, in 
    bike.assign_driver("Robert")
AttributeError: 'Motorcycle' object has no attribute 'assign_driver'

In this case, we can modify the Motorcycle class or the drive.py script to use the right methods. However, in many cases, we may not have access to the source code of the client or service class. Also, this is a simple example. With larger clients and services, it may not be feasible to refactor either of them in case we break compatibility with other systems.

Instead, we can use an adapter to bridge the compatibility gap between our client code and our service object.

Using Adapters to Solve the Incompatibility Issue

In a new file, motorcycle_adapter.py, add the following class:

class MotorcycleAdapter:

    def __init__(self, motorcycle):
        self.motorcycle = motorcycle

    def accelerate(self):
        self.motorcycle.rev_throttle()

    def apply_brakes(self):
        self.motorcycle.pull_brake_lever()

    def assign_driver(self, name):
        self.motorcycle.assign_rider(name)

We created a MotorcycleAdapter class, which instantiates itself with a service object (motorcycle). The adapter implements the client methods which are accelerate(), apply_brakes() and assign_driver(). Inside the body of the accelerate() method, we have used the motorcycle instance of the service object to call the rev_throttle() service method. Likewise, the other methods use the corresponding methods of the Motorcycle class.

Now, let’s update drive.py so we can use the adapter in the try/except block:

from car import Car
from motorcycle import Motorcycle
from motorcycle_adapter import MotorcycleAdapter 
import traceback

if __name__ == '__main__':
    car = Car()
    bike = Motorcycle()
    bike_adapter = MotorcycleAdapter(bike) 

    ...

    try:
        print("Attempting to call client methods with the service object using an adaptern")
        bike_adapter.assign_driver("Robert")
        bike_adapter.accelerate()
        bike_adapter.apply_brakes()
    except AttributeError:
        print("Oops! bike object cannot access car methods")
        traceback.print_exc()

Here,bike_adapter is an object of the MotorcycleAdapter class. We supplied the bike object to the MotorcycleAdapter class’ constructor. Executing this script gives us the following output:

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

The Motorcycle

Subodh is riding the motorcycle
The speed of the motorcycle is 88 mph
The speed of the motorcycle is 35 mph after applying the brakes


The Car

Sushant is driving the car
The speed of the car is 91 mph
The speed of the car is 24 mph after applying the brakes


Attempting to call client methods with the service object

Attempting to call client methods with the service object using an adapter

Robert is riding the motorcyle
The speed of the motorcycle is 67 mph
The speed of the motorcycle is 25 mph after applying the brakes

Without having to adjust the underlying Motorcycle class, we can get it to work like a Car using an adapter!

Pros and Cons of Adapter Design Pattern

The advantages of Adapter Patterns are:

  • We can achieve low coupling between the adapter class and the client class.
  • We can reuse the adapter class to incorporate numerous service classes in the application.
  • We can increase the flexibility of the program by introducing multiple adapters without interfering with the client code

The disadvantages of Adapter Pattern are:

  • The program’s complexity increases with the addition of adapter class and service class.
  • There is an increase of overhead in the program as the requests are forwarded from one class to another.
  • Adapter Pattern(class adapter) uses multiple inheritances, which all the programming languages may not support.

Conclusion

In this article, we learned about the adapter design pattern, its types, and the problems they solve. We implemented the Adapter Pattern in Python so that we can interact with a Motorcycle object, like a Car object by using an adapter so the interface of each class doesn’t change.

Time Stamp:

More from Stackabuse