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()
apply 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 Motorcycle\n")
bike.assign_rider("Subodh")
bike.rev_throttle()
bike.pull_brake_lever()
print("\n")
print("The Car\n")
car.assign_driver("Sushant")
car.accelerate()
car.apply_brakes()
print("\n")
print("Attempting to call client methods with the service object\n")
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 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 <module>
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 the adapter class
import traceback
if __name__ == '__main__':
car = Car()
bike = Motorcycle()
bike_adapter = MotorcycleAdapter(bike) # Create the adapter
...
try:
print("Attempting to call client methods with the service object using an adapter\n")
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 motorcycle
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 inheritance, 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.