Skip to content

Adapter Design Pattern

Video Lecture

Section Video Links
Adapter Overview Adapter Overview Adapter Overview Adapter Overview 
Adapter Use Case Adapter Use Case Adapter Use Case Adapter Use Case 
Python isinstance() Function Python isinstance() Function Python isinstance() Function Python isinstance() Function 
Python time Module Python time Module Python time Module Python time Module 

Overview

Sometimes classes have been written, and you don't have the option of modifying their interface to suit your needs. This happens if the method you are calling is on a different system across a network, a library that you may import or generally something that is not viable to modify directly for your particular needs.

The Adapter design pattern solves these problems:

  • How can a class be reused that does not have an interface that a client requires?
  • How can classes that have incompatible interfaces work together?
  • How can an alternative interface be provided for a class?

You may have two classes that are similar, but they have different method signatures, so you create an Adapter over top of one of the method signatures so that it is easier to implement and extend in the client.

An adapter is similar to the Decorator in the way that it also acts like a wrapper to an object. It is also used at runtime; however, it is not designed to be used recursively.

It is an alternative interface over an existing interface. Furthermore, it can also provide extra functionality that the interface being adapted may not already provide.

The adapter is similar to the Facade, but you are modifying the method signature, combining other methods and/or transforming data that is exchanged between the existing interface and the client.

The Adapter is used when you have an existing interface that doesn't directly map to an interface that the client requires. So, then you create the Adapter that has a similar functional role, but with a new compatible interface.

Terminology

  • Target: The domain specific interface or class that needs to be adapted.
  • Adapter Interface: The interface of the target that the adapter will need to implement.
  • Adapter: The concrete adapter class containing the adaption process.
  • Client: The client application that will use the Adapter.

Adapter UML Diagram

Adapter Pattern UML Diagram

Source Code

In this concept source code, there are two classes, ClassA and ClassB, with different method signatures. Let's consider that ClassA provides the most compatible and preferred interface for the client.

I can create objects of both classes in the client, and it works. But before using each objects method, I need to do a conditional check to see which type of class it is that I am calling since the method signatures are different.

It means that the client is doing extra work. Instead, I can create an Adapter interface for the incompatible ClassB, that reduces the need for the extra conditional logic.

./adapter/adapter_concept.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# pylint: disable=too-few-public-methods
"Adapter Concept Sample Code"
from abc import ABCMeta, abstractmethod

class IA(metaclass=ABCMeta):
    "An interface for an object"
    @staticmethod
    @abstractmethod
    def method_a():
        "An abstract method A"

class ClassA(IA):
    "A Sample Class the implements IA"

    def method_a(self):
        print("method A")

class IB(metaclass=ABCMeta):
    "An interface for an object"
    @staticmethod
    @abstractmethod
    def method_b():
        "An abstract method B"

class ClassB(IB):
    "A Sample Class the implements IB"

    def method_b(self):
        print("method B")

class ClassBAdapter(IA):
    "ClassB does not have a method_a, so we can create an adapter"

    def __init__(self):
        self.class_b = ClassB()

    def method_a(self):
        "calls the class b method_b instead"
        self.class_b.method_b()

# The Client
# Before the adapter I need to test the objects class to know which
# method to call.
ITEMS = [ClassA(), ClassB()]
for item in ITEMS:
    if isinstance(item, ClassB):
        item.method_b()
    else:
        item.method_a()

# After creating an adapter for ClassB I can reuse the same method
# signature as ClassA (preferred)
ITEMS = [ClassA(), ClassBAdapter()]
for item in ITEMS:
    item.method_a()

Output

1
2
3
4
5
python ./adapter/adapter_concept.py
method A
method B
method A
method B

Example Use Case

The example client can manufacture a Cube using different tools. Each solution is invented by a different company. The client user interface manages the Cube product by indicating the width, height and depth. This is compatible with the company A that produces the Cube tool, but not the company B that produces their own version of the Cube tool that uses a different interface with different parameters.

In this example, the client will re-use the interface for company A's Cube and create a compatible Cube from company B.

An adapter will be needed so that the same method signature can be used by the client without the need to ask company B to modify their Cube tool for our specific domains use case.

My imaginary company needs to use both cube suppliers since there is a large demand for cubes and when one supplier is busy, I can then ask the other supplier.

Example UML Diagram

Adapter Pattern in Context

Source Code

./adapter/client.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"Adapter Example Use Case"

import time
import random
from cube_a import CubeA
from cube_b_adapter import CubeBAdapter

# client
TOTALCUBES = 5
COUNTER = 0
while COUNTER < TOTALCUBES:
    # produce 5 cubes from which ever supplier can manufacture it first
    WIDTH = random.randint(1, 10)
    HEIGHT = random.randint(1, 10)
    DEPTH = random.randint(1, 10)
    CUBE = CubeA()
    SUCCESS = CUBE.manufacture(WIDTH, HEIGHT, DEPTH)
    if SUCCESS:
        print(
            f"Company A building Cube id:{id(CUBE)}, "
            f"{CUBE.width}x{CUBE.height}x{CUBE.depth}")
        COUNTER = COUNTER + 1
    else:  # try other manufacturer
        print("Company A is busy, trying company B")
        CUBE = CubeBAdapter()
        SUCCESS = CUBE.manufacture(WIDTH, HEIGHT, DEPTH)
        if SUCCESS:
            print(
                f"Company B building Cube id:{id(CUBE)}, "
                f"{CUBE.width}x{CUBE.height}x{CUBE.depth}")
            COUNTER = COUNTER + 1
        else:
            print("Company B is busy, trying company A")
    # wait some time before manufacturing a new cube
    time.sleep(1)

print(f"{TOTALCUBES} cubes have been manufactured")

./adapter/cube_a.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# pylint: disable=too-few-public-methods
"A Class of Cube from Company A"
import time
from interface_cube_a import ICubeA

class CubeA(ICubeA):
    "A hypothetical Cube tool from company A"
    # a static variable indicating the last time a cube was manufactured
    last_time = int(time.time())

    def __init__(self):
        self.width = self.height = self.depth = 0

    def manufacture(self, width, height, depth):
        self.width = width
        self.height = height
        self.depth = depth
        # if not busy, then manufacture a cube with dimensions
        now = int(time.time())
        if now > int(CubeA.last_time + 1):
            CubeA.last_time = now
            return True
        return False  # busy

./adapter/cube_b.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# pylint: disable=too-few-public-methods
"A Class of Cube from Company B"
import time
from interface_cube_b import ICubeB

class CubeB(ICubeB):
    "A hypothetical Cube tool from company B"
    # a static variable indicating the last time a cube was manufactured
    last_time = int(time.time())

    def create(self, top_left_front, bottom_right_back):
        now = int(time.time())
        if now > int(CubeB.last_time + 2):
            CubeB.last_time = now
            return True
        return False  # busy

./adapter/cube_b_adapter.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# pylint: disable=too-few-public-methods
"An adapter for CubeB so that it can be used like Cube A"
from interface_cube_a import ICubeA
from cube_b import CubeB

class CubeBAdapter(ICubeA):
    "Adapter for CubeB that implements ICubeA"

    def __init__(self):
        self.cube = CubeB()
        self.width = self.height = self.depth = 0

    def manufacture(self, width, height, depth):
        self.width = width
        self.height = height
        self.depth = depth

        success = self.cube.create(
            [0-width/2, 0-height/2, 0-depth/2],
            [0+width/2, 0+height/2, 0+depth/2]
        )
        return success

./adapter/interface_cube_a.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# pylint: disable=too-few-public-methods
"An interface to implement"
from abc import ABCMeta, abstractmethod

class ICubeA(metaclass=ABCMeta):
    "An interface for an object"
    @staticmethod
    @abstractmethod
    def manufacture(width, height, depth):
        "manufactures a cube"

./adapter/interface_cube_b.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# pylint: disable=too-few-public-methods
"An interface to implement"
from abc import ABCMeta, abstractmethod

class ICubeB(metaclass=ABCMeta):
    "An interface for an object"
    @staticmethod
    @abstractmethod
    def create(top_left_front, bottom_right_back):
        "Manufactures a Cube with coords offset [0, 0, 0]"

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
python ./adapter/client.py
Company A is busy, trying company B
Company B is busy, trying company A
Company A is busy, trying company B
Company B is busy, trying company A
Company A building Cube id:2968196317136, 2x3x7
Company A is busy, trying company B
Company B building Cube id:2968196317136, 8x2x8
Company A building Cube id:2968196317040, 4x6x4
Company A is busy, trying company B
Company B is busy, trying company A
Company A building Cube id:2968196317136, 5x4x8
Company A is busy, trying company B
Company B building Cube id:2968196317136, 2x2x9
5 cubes have been manufactured

New Coding Concepts

Python isinstance() Function

Syntax: isinstance(object, type)

Returns: True or False

You can use the inbuilt function isinstance() to conditionally check the type of an object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> isinstance(1,int)
True
>>> isinstance(1,bool)
False
>>> isinstance(True,bool)
True
>>> isinstance("abc",str)
True
>>> isinstance("abc",(int,list,dict,tuple,set))
False
>>> isinstance("abc",(int,list,dict,tuple,set,str))
True

You can also test your custom classes.

1
2
3
4
5
6
7
class my_class:
    "nothing to see here"

CLASS_A = my_class()
print(type(CLASS_A))
print(isinstance(CLASS_A, bool))
print(isinstance(CLASS_A, my_class))

Outputs

1
2
3
<class '__main__.my_class'>
False
True

You can use it in logical statements as I do in adapter_concept.py above.

Python time Module

The time module provides time related functions, most notably in my case, the current epoch (ticks) since January 1, 1970, 00:00:00 (UTC).

The time module provides many options that are outlined in more detail at https://docs.python.org/3/library/time.html

In ./adapter/cube_a.py, I check the time.time() at various intervals to compare how long a task took.

1
2
3
4
    now = int(time.time())
    if now > int(CubeA.last_time + 1):
        CubeA.last_time = now
        return True

I also use the time module to sleep for a second between loops to simulate a 1-second delay. See ./adapter/client.py

1
2
    # wait some time before manufacturing a new cube
    time.sleep(1)

When executing ./adapter/cube_a.py you will notice that the process will run for about 10 seconds outputting the gradual progress of the construction of each cube.

Summary

  • Use the Adapter when you want to use an existing class, but its interface does not match what you need.
  • The adapter adapts to the interface of its parent class for those situations when it is not viable to modify the parent class to be domain-specific for your use case.
  • Adapters will most likely provide an alternative interface over an existing object, class or interface, but it can also provide extra functionality that the object being adapted may not already provide.
  • An adapter is similar to a Decorator except that it changes the interface to the object, whereas the decorator adds responsibility without changing the interface. This also allows the Decorator to be used recursively.
  • An adapter is similar to the Bridge pattern and may look identical after the refactoring has been completed. However, the intent of creating the Adapter is different. The Bridge is a result of refactoring existing interfaces, whereas the Adapter is about adapting over existing interfaces that are not viable to modify due to many existing constraints. E.g., you don't have access to the original code, or it may have dependencies that already use it and modifying it would affect those dependencies negatively.