Wprowadzenie
Adapter jest strukturalnym wzorcem projektowym, którego celem jest umożliwienie współpracy obiektów posiadających niekompatybilne interfejsy.
Dzięki zastosowaniu tego wzorca możemy połączyć ze sobą klasy, które w obecnej postaci nie byłyby w stanie komunikować się bez modyfikacji istniejącego kodu.
Problem
Otrzymaliśmy zadanie polegające na dostosowaniu systemu do nowego interfejsu dostępowego miejskiej biblioteki. Aktualnie nasza aplikacja korzysta z określonego interfejsu API, z którym jest bezpośrednio zintegrowana poprzez własną warstwę komunikacyjną.
Nowe API zmieniło kontrakt metod: zamiast zwracać informację o dostępności w postaci wartości logicznej, zwraca liczbę dostępnych egzemplarzy. Dodatkowo zmieniono sposób identyfikacji użytkownika – wcześniejsze dane osobowe zostały zastąpione pojedynczym identyfikatorem w postaci adresu e-mail. W efekcie istniejący kod przestał być zgodny z nowym interfejsem.
Na pierwszy rzut oka mogłoby się wydawać, że wystarczy zmodyfikować istniejący kod tak, aby korzystał z nowego interfejsu. Jednak takie podejście oznaczałoby ingerencję w sprawdzoną i działającą logikę biznesową.
Modyfikowanie istniejącego kodu niesie ze sobą ryzyko wprowadzenia błędów, a także może naruszać zasadę otwarte/zamknięte (Open/Closed Principle), zgodnie z którą klasy powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje.
Rozwiązanie
W takiej sytuacji pomocny okazuje się wzorzec projektowy Adapter.
Adapter implementuje dotychczasowy interfejs wykorzystywany przez system, dzięki czemu istniejące klasy nie wymagają żadnych zmian. Jednocześnie wewnętrznie korzysta z nowego API, delegując do niego wywołania metod i dokonując niezbędnej konwersji danych.
Takie rozwiązanie pozwala:
- zachować zgodność z zasadą pojedynczej odpowiedzialności (SRP) – logika konwersji danych zostaje wydzielona do osobnej klasy,
- spełnić zasadę otwarte/zamknięte (OCP) – rozszerzamy system poprzez dodanie nowej klasy, zamiast modyfikować istniejące,
- zminimalizować ryzyko wprowadzenia błędów do działającego kodu.
Implementacja
Struktura wzorca
Diagram UML przedstawia schemat działania systemu przed pojawieniem się nowej wersji API.
Klasa BookConnector pełni rolę klienta (Client) i komunikuje się bezpośrednio z interfejsem LibraryAPI (Target), który definiuje kontrakt dostępu do biblioteki.
Zależność wygląda następująco:

Wraz z pojawieniem się nowego API (Adaptee) zmienił się kontrakt metod — zarówno w zakresie argumentów, jak i typów zwracanych wartości.
Aby uniknąć modyfikacji klasy BookConnector, wprowadzono wzorzec Adapter.
Nowa struktura wygląda następująco:

Klasa LibraryAPIAdapter:
- implementuje stary interfejs (
LibraryAPI), - wewnętrznie korzysta z nowego API (
LibraryAPINew), - tłumaczy wywołania metod oraz przekształca argumenty i wartości zwracane.
Dzięki temu klasa BookConnector pozostaje niezmieniona i nadal korzysta z tego samego kontraktu.
Struktura plików
- adapter
- user.py -> definiuje encję domenową (
User) - library_api.py -> stary interfejs (
LibraryAPI) - library_api_new.py -> nowe API (
LibraryAPINew) - library_api_adapter.py -> Adapter implementujący stary interfejs i tłumaczący wywołania do nowego API
- book_connector.py -> klient, który korzysta ze starego interfejsu API
- main.py -> przykład użycia
- user.py -> definiuje encję domenową (
Kod źródłowy
user.py
from datetime import date
from dataclasses import dataclass
@dataclass
class User:
first_name: str
last_name: str
email: str
date_of_birth: date
def __str__(self) -> str:
return f"{self.first_name} {self.last_name} ({self.email})"
Code language: Python (python)
library_api.py
from abc import ABC, abstractmethod
from datetime import date
class LibraryAPI(ABC):
@abstractmethod
def is_book_available(self, isbn: str) -> bool:
pass
@abstractmethod
def get_return_date(
self,
isbn: str,
first_name: str,
last_name: str,
date_of_birth: date
) -> date:
pass
@abstractmethod
def reserve_book(
self,
isbn: str,
first_name: str,
last_name: str,
date_of_birth: date
) -> bool:
pass
Code language: Python (python)
library_api_new.py
from datetime import date, timedelta
class LibraryAPINew:
def number_of_available_copies(self, isbn: str) -> int:
print(f"Checking copies for ISBN: {isbn}...")
return 3
def get_return_date(self, isbn: str, email: str) -> date:
print(f"Getting return date for {email}, ISBN: {isbn}...")
return date.today() + timedelta(days=14)
def reserve_book(self, isbn: str, email: str) -> bool:
print(f"Reserving book for {email}, ISBN: {isbn}...")
return True
Code language: Python (python)
library_api_adapter.py
from datetime import date
from library_api import LibraryAPI
from library_api_new import LibraryAPINew
class LibraryAPIAdapter(LibraryAPI):
def __init__(self, new_api: LibraryAPINew):
self._new_api = new_api
def is_book_available(self, isbn: str) -> bool:
# The NEW API returns an int, but the old contract requires a bool
return self._new_api.number_of_available_copies(isbn) > 0
def get_return_date(
self,
isbn: str,
first_name: str,
last_name: str,
date_of_birth: date
) -> date:
# In a production environment, the email would be retrieved from the database.
# Here we generate it artificially solely for demonstration purposes.
email = self._build_email(first_name, last_name)
return self._new_api.get_return_date(isbn, email)
def reserve_book(
self,
isbn: str,
first_name: str,
last_name: str,
date_of_birth: date
) -> bool:
email = self._build_email(first_name, last_name)
return self._new_api.reserve_book(isbn, email)
def _build_email(self, first_name: str, last_name: str) -> str:
return f"{first_name.lower()}.{last_name.lower()}@example.com"
Code language: Python (python)
book_connector.py
from datetime import date
from library_api import LibraryAPI
from user import User
class BookConnector:
def __init__(self, api: LibraryAPI):
self._api = api
def check_availability(self, isbn: str) -> bool:
return self._api.is_book_available(isbn)
def check_return_date(self, isbn: str, user: User) -> date:
return self._api.get_return_date(
isbn,
user.first_name,
user.last_name,
user.date_of_birth
)
def reserve(self, isbn: str, user: User) -> bool:
return self._api.reserve_book(
isbn,
user.first_name,
user.last_name,
user.date_of_birth
)
Code language: Python (python)
main.py
from datetime import date
from library_api_new import LibraryAPINew
from library_api_adapter import LibraryAPIAdapter
from book_connector import BookConnector
from user import User
if __name__ == "__main__":
isbn = "978-3-16-148410-0"
user = User(
"Mateusz",
"Przybyla",
"mateusz.przybyla@example.com",
date(1995, 5, 12)
)
print("--- Using new API via Adapter ---")
new_api = LibraryAPINew()
adapter = LibraryAPIAdapter(new_api)
connector = BookConnector(adapter)
print("Available:", connector.check_availability(isbn))
print("Reserved:", connector.reserve(isbn, user))
print("Return date:", connector.check_return_date(isbn, user))
Code language: Python (python)Wynik działania:
--- Using new API via Adapter ---
Checking copies for ISBN: 978-3-16-148410-0...
Available: True
Reserving book for mateusz.przybyla@example.com, ISBN: 978-3-16-148410-0...
Reserved: True
Getting return date for mateusz.przybyla@example.com, ISBN: 978-3-16-148410-0...
Return date: 2026-03-08
Code language: Python (python)
Podsumowanie działania
W przedstawionym przykładzie klasa BookConnector nie została zmodyfikowana mimo zmiany interfejsu biblioteki. System nadal korzysta ze starego kontraktu (LibraryAPI), który definiuje oczekiwane metody oraz ich sygnatury.
Nowe API (LibraryAPINew) posiada inny sposób komunikacji – zmienione typy zwracane oraz inny sposób identyfikacji użytkownika. Zamiast modyfikować istniejącą logikę biznesową, wprowadzono klasę LibraryAPIAdapter, która:
- implementuje stary interfejs,
- deleguje wywołania do nowego API,
- konwertuje dane wejściowe i wyjściowe do postaci oczekiwanej przez system.
Adapter pełni rolę warstwy pośredniej izolującej zmiany w zewnętrznym systemie od logiki aplikacji.
Kiedy nie używać wzorca
Mimo swojej użyteczności, wzorzec Adapter nie zawsze jest najlepszym rozwiązaniem.
Nie warto go stosować, gdy:
- możemy bezpiecznie zmodyfikować istniejący interfejs,
- różnice między interfejsami są minimalne i nie wymagają rozbudowanego pośrednika,
- wprowadzanie dodatkowej warstwy zwiększa niepotrzebnie złożoność systemu.
Wzorzec ten powinien być stosowany przede wszystkim wtedy, gdy:
- integrujemy się z zewnętrznym systemem,
- nie mamy możliwości modyfikacji istniejącego kodu,
- zależy nam na zachowaniu zgodności z zasadą otwarte/zamknięte.
