9.3. Adapter
EN: Adapter
PL: Adapter
Type: class and object
The Adapter design pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It wraps an existing class with a new interface so that it becomes compatible with the client's interface.
Here's a simple example of the Adapter pattern in Python:
>>> class Target:
... def request(self):
... return "Target: The default target's behavior."
...
>>> class Adaptee:
... def specific_request(self):
... return ".eetpadA eht fo roivaheb laicepS"
...
>>> class Adapter(Target, Adaptee):
... def request(self):
... return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"
...
>>> def client_code(target: "Target") -> None:
... print(target.request(), end="")
...
>>> adaptee = Adaptee()
>>> print("Adaptee: " + adaptee.specific_request())
Adaptee: .eetpadA eht fo roivaheb laicepS
>>> adapter = Adapter()
>>> client_code(adapter)
Adapter: (TRANSLATED) Special behavior of the Adaptee.
In this example, Target is the interface that the client uses, Adaptee is the class with an incompatible interface that needs adapting, and Adapter is the class that adapts the Adaptee to the Target interface. The Adapter class inherits from both Target and Adaptee and overrides the request method of Target to call the specific_request method of Adaptee. The client_code function works with objects of the Target class. It remains unaware of the Adapter and Adaptee classes as long as the Adapter follows the Target interface.
9.3.1. Pattern
Convert an interface of an object to a different form
Like power socket adapter for US and EU
Refactoring of a large application
Working with legacy code / database
Niekompatybilne API dwóch systemów
Wymagające różnych sposobów uwierzytelniania (OAuth2, BasicAuth)
Tłumaczenie pomiędzy różnymi formatami danych (SOAP/XML, REST/JSON)
Iteracyjne przepisywanie legacy systemu na nowy, ale tak, aby móc wciąż korzystać ze starego
9.3.2. Problem
BlackAndWhite3rdPartyFilter
is from external libraryDoes not conform to
Filter
interfaceDo not have
apply()
methodNeed manual call of
init()
at initializationNeed manual call of
render()
from abc import ABC, abstractmethod
from pathlib import Path
# %% 3rd Party
class AmazingFilter:
def __init__(self):
print('Setting up filter')
def transform(self, content: bytes) -> bytes:
print('Making your phot amazing')
return content
# %% Abstract
class Filter(ABC):
@abstractmethod
def apply(self, content: bytes) -> bytes:
...
# %% Implementation
class SepiaFilter(Filter):
def apply(self, content: bytes) -> bytes:
print('Making photo in sepia')
return content
class SharpenFilter(Filter):
def apply(self, content: bytes) -> bytes:
print('Making photo sharp')
return content
# %% Main
class Image:
path: Path
content: bytes
def __init__(self, path):
self.path = Path(path)
self.content = self.path.read_bytes()
def apply(self, filter: Filter) -> None:
self.content = filter.apply(self.content)
if __name__ == '__main__':
img = Image('/tmp/myfile.png')
img.apply(SepiaFilter())
img.apply(SharpenFilter())
img.apply(AmazingFilter())
# AttributeError: 'AmazingFilter' object has no attribute 'apply'
9.3.3. Solution
Inheritance is simpler
Composition is more flexible
Favor Composition over Inheritance
from abc import ABC, abstractmethod
from pathlib import Path
# %% 3rd Party
class AmazingFilter:
def __init__(self):
print('Setting up filter')
def transform(self, content: bytes) -> bytes:
print('Making your phot amazing')
return content
# %% Abstract
class Filter(ABC):
@abstractmethod
def apply(self, content: bytes) -> bytes:
...
# %% Implementation
class SepiaFilter(Filter):
def apply(self, content: bytes) -> bytes:
print('Making photo in sepia')
return content
class SharpenFilter(Filter):
def apply(self, content: bytes) -> bytes:
print('Making photo sharp')
return content
# %% Adapter
class AmazingFilterAdapter(Filter):
filter: AmazingFilter
def __init__(self):
self.filter = AmazingFilter()
def apply(self, content: bytes) -> bytes:
self.filter.__init__()
return self.filter.transform(content)
# %% Main
class Image:
path: Path
content: bytes
def __init__(self, path):
self.path = Path(path)
self.content = self.path.read_bytes()
def apply(self, filter: Filter) -> None:
self.content = filter.apply(self.content)
if __name__ == '__main__':
img = Image('/tmp/myfile.png')
img.apply(SepiaFilter())
img.apply(SharpenFilter())
img.apply(AmazingFilterAdapter())
9.3.4. Use Case - 1
>>> def otherrange(a, b, c): # function with bad API
... current = a
... result = []
... while current < b:
... result.append(current)
... current += c
... return result
>>>
>>>
>>> def myrange(start, stop, step): # adapter
... return otherrange(a=start, b=stop, c=step)
>>>
>>>
>>> myrange(start=10, stop=20, step=2)
[10, 12, 14, 16, 18]