Design Patterns: Adapter

In this post, an adapter pattern is explained. Original code in Java can be found in a book, I “増補改訂版Java言語で学ぶデザインパターン入門”.

An adapter works as a middleware between an adaptee and a target, each has its own methods. If an adaptee and a target are incompatible with each other, an adapter bridges a gap between the two objects. We could achieve this in 2 ways; 1. adapter by inheritance, 2. adapter by delegation.

Let’s see the concrete examples.

First, we create an adapter by inheritance. In this program, Banner class is defined as an adaptee, Printing interface as a target and PrintBanner as an adapter.

Banner is a provided class that has two methods to modify and print strings. Printing interface is an interface that a client uses and that has two abstract methods to modify and print strings.

# banner.py
class Banner:
    def __init__(self, string):
        self.string = string
    
    def show_with_paren(self):
        print(f"( {self.string} )")
    
    def show_with_aster(self):
        print(f"* {self.string} *")
# printing.py
from abc import ABC, abstractmethod

class Printing(ABC):
    @abstractmethod
    def print_weak(self):
        pass
    @abstractmethod
    def print_strong(self):
        pass

PrintBanner class extends Banner class and implements Printing Interface.

# print_banner.py
from banner import Banner
from printing import Printing

class PrintBanner(Printing, Banner):
    def __init__(self, string):
        super().__init__(string)
    
    def print_weak(self):
        super().show_with_paren()

    def print_strong(self):
        super().show_with_aster()

Let’s call PrintBanner class. It is clear that an abstraction works in a way that it hides an existence of Banner class. It means that we can change an adaptee and the program still works correctly without changing its internal methods or implementation as long as an adapter is properly set (we only need to change an adapter) .

# main.py
from print_banner import PrintBanner 

def main():
    p = PrintBanner("Hello")
    assert isinstance(p, Printing)
    p.print_weak()
    p.print_strong()

if __name__ == "__main__":
    main()
( Hello )
* Hello *

That was an adapter design by inheritance. Now let’s define an adapter by delegation.

In this way, we create an instance of an adaptee in an adapter and let an adaptee execute its own methods as opposed to extending an adaptee and calling methods.

# printing.py
from abc import ABC, abstractmethod

class Printing(ABC):
    @abstractmethod
    def print_weak(self):
        pass
    @abstractmethod
    def print_strong(self):
        pass
# print_banner.py
from printing import Printing
from banner import Banner

class PrintBanner(Printing):
    def __init__(self, string):
        self.banner = Banner(string)

    def print_weak(self):
        self.banner.show_with_paren()

    def print_strong(self):
        self.banner.show_with_aster()

One benefit of an adapter pattern: we do no need to modify existing code in an adaptee class or module when a new interface or API is needed. If we are sure that an adaptee is fully-tested and works properly, we do not want to change it since it may cause some bugs. We can only add an adapter so that an adaptee can communicate with an interface properly.

Leave a comment