Po co w ogóle wzorce projektowe programiście Pythona i TypeScriptu?
Wspólny język zamiast złotego młotka
Wzorce projektowe to przede wszystkim słownictwo, a dopiero później konkretne schematy kodu. Zespół, który potrafi powiedzieć „zróbmy tu strategię” albo „to wygląda na adapter”, szybciej się dogaduje, szczególnie jeśli miesza się w nim Python, TypeScript i różne poziomy doświadczenia.
Nie są to recepty idealne na każdy ból. Te same problemy w Pythonie często da się rozwiązać krótkim idiomem, a w TypeScripcie – czytelnym interfejsem i typowaniem. Ważny jest rozpoznany problem, a nie ślepe dopasowywanie definicji z książki GoF („Gang of Four”) do każdego fragmentu kodu.
Kiedy wzorce projektowe naprawdę pomagają:
gdy projekt rośnie i pojawia się wiele podobnych, ale nieidentycznych fragmentów logiki,
gdy trzeba zapanować nad zależnościami pomiędzy modułami w kilku usługach i aplikacjach (web, worker, front),
gdy kilka zespołów dotyka tego samego kodu i trzeba szybko tłumaczyć, „co się tu właściwie dzieje”.
Definicja vs rozpoznawanie problemu
Znajomość definicji typu „Strategy pozwala enkapsulować algorytmy i wymieniać je w czasie działania” jest mało użyteczna, jeśli nie prowadzi do wniosków w konkretnym kodzie. Kluczowe pytanie brzmi: czy rozpoznajesz sytuację, w której dany wzorzec faktycznie pomaga?
Rozróżnienie jest dość ostre:
tryb teoretyczny: „Strategy to interfejs + implementacje + kontekst, który je wykorzystuje”,
tryb praktyczny: „w tym module jest 12 if-ów wybierających sposób liczenia ceny – to aż prosi się o strategie”.
W projektach produkcyjnych realny zysk z wzorców pojawia się dopiero wtedy, gdy łączy się nazwę wzorca z typowym „code smell” – sygnałem, że coś zaczyna być trudne w utrzymaniu (rozrastające się warunki, duplikacja, klasy-„bąble” robiące wszystko naraz).
Od książki GoF do Pythona i TypeScriptu
Oryginalne wzorce projektowe GoF powstawały z myślą o językach takich jak C++ czy Java z lat 90. Od tamtej pory:
Python wprowadził dekoratory, moduły, bogate typowanie opcjonalne, metaklasy i protokoły.
TypeScript dołożył system typów, interfejsy, union types, klasy, dekoratory (eksperymentalne) i moduły ES.
Część wzorców „rozpuściła się” w samych językach: funkcje wyższego rzędu zastępują proste strategie, moduły stają się naturalnymi singletonami, dekoratory upraszczają wiele zastosowań dekoratora z GoF. Inne wzorce, szczególnie strukturalne (Adapter, Facade), pozostają bardzo aktualne w świecie mikroserwisów i bogatych API.
Co wiemy: wzorce projektowe redukują chaos w większych systemach i ułatwiają komunikację. Czego nie wiemy od razu: które z nich naprawdę są potrzebne w konkretnym zespole, w konkretnym staku (Python backend, TypeScript frontend czy Node). Odpowiedź da dopiero codzienny kod, a nie teoretyczna lista „trzeba znać 23 wzorce GoF”.
Kontekst technologiczny: Python vs TypeScript a projektowanie
Dynamiczny Python i statyczny TypeScript
Python i TypeScript różnią się fundamentalnie pod względem typowania i ducha języka:
Python – typowanie dynamiczne, silne, z opcjonalnym statycznym sprawdzaniem (mypy, pyright). Elastyczność, duck-typing, proste funkcje jako obiekty pierwszej klasy.
TypeScript – nadzbiór JavaScriptu z rozbudowanym statycznym systemem typów: interfejsy, klasy, typy unii, generics. Co jest typowo sprawdzane przy kompilacji, a nie w runtime.
Ten kontrast bezpośrednio wpływa na to, jak implementuje się wzorce projektowe. W Pythonie wiele rzeczy można zrealizować poprzez konwencję i duck-typing („ma metodę send, więc traktujmy to jak klienta API”), a w TypeScripcie często opłaca się wyrazić to jasno w interfejsie i dać kompilatorowi możliwość złapania niezgodności.
Wzorce wbudowane w idiomy języka
Część wzorców projektowych jest tak naturalna dla danego języka, że mówienie o „wzorcu” bywa przesadą:
Python: dekoratory funkcji i klas realizują wiele przypadków wzorca Decorator bez dodatkowej struktury. Moduły pełnią rolę Singletona. Funkcje wyższego rzędu i zamknięcia pokrywają dużą część Strategy.
TypeScript: interfejsy i klasy w stylu „konfiguruj przez konstruktor” tworzą naturalne miejsce na wiele wzorców kreacyjnych i strukturalnych. Union types plus narrowing upraszczają wiele wzorców, które w językach bez unii byłyby rozbudowanymi hierarchiami klas.
Wzorce nie znikają, ale ich kształt w kodzie się zmienia. W Pythonie często wystarczy prosty słownik strategii lub funkcja-fabryka, w TypeScripcie dochodzi do tego opis w systemie typów i integracja z narzędziami (IDE, lint, kompilator).
Frameworki a wzorce: nie kopiować tego, co już jest
Django, FastAPI, Flask po stronie Pythona oraz NestJS, Angular, React po stronie TypeScriptu intensywnie korzystają ze wzorców: Factory, Dependency Injection, Template Method, Observer, Facade. Duży margines błędu powstaje wtedy, gdy projekt duplikuje te same mechanizmy własnymi implementacjami.
Kilka przykładów:
Django – już używa wzorców jak Template Method (generic views), Factory (ORM tworzący obiekty), Singleton (ustawienia). Dodawanie własnej wielopoziomowej hierarchii fabryk nad modelami często tylko komplikuje struktury.
FastAPI – ma wbudowany mechanizm „dependency injection” funkcjami. To praktyczne zastosowanie kilku wzorców (Provider, Factory). Tworzenie obok tego własnego, ręcznego kontenera DI bywa powielaniem wysiłku.
NestJS – to de facto framework oparty na DI, modułach, kontrolerach i serwisach. Wiele wzorców strukturalnych jest tu gotowych. Dodatkowe „fabryki serwisów” nad serwisami rzadko pomagają.
Punktem wyjścia powinno być pytanie: czy idiom/framework już tego nie rozwiązuje? Dopiero jeśli odpowiedź brzmi „nie”, sięgnięcie po klasyczny wzorzec ma sens.
Źródło: Pexels | Autor: Startup Stock Photos
Jak czytać i „tłumaczyć” wzorce projektowe na konkretny kod
Struktura opisu wzorca ważniejsza niż UML
Użyteczny opis wzorca zwykle składa się z kilku elementów:
Problem – typowa sytuacja, w której kod „zaczyna boleć” (np. wiele warunków, trudne testowanie, silna zależność od konkretnej biblioteki).
Siły – ograniczenia, z którymi trzeba się pogodzić: stabilność API, wymagania biznesowe, potrzeba rozszerzalności.
Rozwiązanie – ogólna struktura: jakie obiekty, jakie role, kto z kim rozmawia.
Konsekwencje – co się poprawia, a co komplikuje (np. więcej małych klas, rozproszenie logiki).
UML może pomóc, ale w Pythonie i TypeScripcie ważniejsze jest, by przełożyć tę strukturę na czytelne moduły, pliki i nazwy. Wzorzec, który na diagramie wygląda klarownie, w projekcie o dziwnej strukturze katalogów może być trudny do odczytania.
Ten sam wzorzec w Pythonie i TypeScript
Weźmy prosty przykład – Strategy. W Pythonie:
strategią może być klasa z metodą calculate,
albo po prostu funkcja o odpowiednim podpisie,
albo obiekt spełniający protokół z typing (Protocol).
W TypeScripcie:
strategię można opisać interfejsem PricingStrategy,
wdrożyć ją klasami,
albo zdefiniować jako typ funkcji (type PricingStrategy = (input: ...) => number).
Cel jest ten sam: łatwa wymiana algorytmu. Forma dopasowuje się do języka i jego narzędzi. To „tłumaczenie” jest kluczowe: znajdowanie odpowiednika struktury wzorca w idiomie konkretnego języka, a nie kopiowanie czystej wersji z książki wbrew składni i zwyczajom.
Od „code smell” do wzorca, a nie odwrotnie
Praktyczna metoda pracy z wzorcami w Pythonie i TypeScript:
Najpierw namierz „smell” w kodzie: powtarzalne if-y, rozrastające się klasy, funkcje na 200 linii, powtarzane bloki logiki.
Następnie zadaj pytanie: „czy to przypomina problem, który jakiś wzorzec opisuje?”.
Dopasuj prostą, idiomatyczną formę wzorca w danym języku.
Sprawdź, czy kod po refaktoryzacji jest naprawdę czytelniejszy i łatwiejszy do testowania.
Przykład typowego kandydata – powtarzające się if typ == ....
Każdy nowy kanał wymaga nowego elif, testy mieszają się ze sobą, funkcja rośnie. To klasyczna sytuacja, w której Strategy lub prosta mapa strategii (słownik) porządkują logikę.
Podobny „smell” w TypeScripcie
function sendNotification(kind: string, data: any): void {
if (kind === "email") {
// logika wysyłki maila
} else if (kind === "sms") {
// logika wysyłki SMS
} else if (kind === "push") {
// logika wysyłki powiadomienia push
} else {
throw new Error(`Unknown kind: ${kind}`);
}
}
Tutaj również aż prosi się o wydzielenie interfejsu lub typu funkcji i zestawu strategii. Różnica polega na tym, że TypeScript może dodatkowo chronić nas typami (np. type NotificationKind = "email" | "sms" | "push"), co ogranicza ryzyko literówek i nieobsłużonych przypadków.
Wzorzec 1 – Factory / Simple Factory: porządkowanie tworzenia obiektów
Kiedy fabryka ma sens
Fabryka (Simple Factory, czasem Factory Method) porządkuje logikę, która:
rozsiana jest po wielu miejscach (new KlientApi(...) w kilku modułach),
zawiera warunki decydujące, jaki konkretnie obiekt stworzyć,
łączy się z konfiguracją, środowiskiem, flagami feature’ów.
Bez fabryki kod tworzący obiekty:
duplikuje się,
jest trudny do zmiany (nowy typ obiektu = zmiana w wielu plikach),
bywa ciężki w testowaniu (trzeba stubować wiele miejsc naraz).
Prosta fabryka w Pythonie: funkcja czy klasa?
W Pythonie nie trzeba od razu tworzyć klasy fabryki. Najczęściej wystarczy:
funkcja-fabryka,
albo moduł z funkcjami generującymi obiekty.
Przykład: wybór klienta API w Pythonie
Załóżmy, że aplikacja ma mówić do różnych backendów: REST, GraphQL, lub używać klienta-podróbki w testach.
reszta systemu zależy wyłącznie od protokołu ApiClient,
nowy typ klienta wymaga dopisania jednej gałęzi w fabryce.
Interpretacja jest prosta: w małym projekcie taka funkcja wystarcza na długo, a przy rozroście można ją rozbić na moduły lub zamienić na rejestr strategii.
Analogiczna fabryka w TypeScripcie: typy na pierwszej linii
W TypeScripcie fabryka szybko zderza się z typami. Dobrze, jeśli od razu je wykorzystuje:
type ApiClientKind = "rest" | "graphql" | "mock";
interface ApiClient {
getUser(userId: string): Promise<Record<string, unknown>>;
}
class RestApiClient implements ApiClient {
constructor(private readonly baseUrl: string) {}
async getUser(userId: string) {
// HTTP GET `${this.baseUrl}/users/${userId}`
return {};
}
}
class GraphqlApiClient implements ApiClient {
constructor(private readonly endpoint: string) {}
async getUser(userId: string) {
// zapytanie GraphQL
return {};
}
}
class MockApiClient implements ApiClient {
async getUser(userId: string) {
return { id: userId, name: "Test User" };
}
}
function createApiClient(kind: ApiClientKind): ApiClient {
switch (kind) {
case "rest":
return new RestApiClient("https://api.example.com");
case "graphql":
return new GraphqlApiClient("https://gql.example.com");
case "mock":
return new MockApiClient();
}
}
Co wiemy?
kompilator pilnuje, by wszystkie wartości z unii ApiClientKind były obsłużone,
konsumenci widzą tylko interfejs ApiClient, a nie konkretne klasy,
testy mogą wstrzykiwać MockApiClient lub nadpisać fabrykę.
W większym systemie tę funkcję często zastępuje konfiguracja modułów (NestJS), ale struktura pozostaje: jeden punkt tworzenia, wspólny interfejs.
Fabryka jako warstwa nad konfiguracją
Gdy konfiguracja staje się głównym sterownikiem zachowania, fabryka zamienia się w cienką warstwę tłumaczącą dane konfiguracyjne na obiekty.
from dataclasses import dataclass
from typing import Literal
ClientKind = Literal["rest", "graphql", "mock"]
@dataclass
class ApiConfig:
kind: ClientKind
base_url: str | None = None
endpoint: str | None = None
def create_api_client_from_config(cfg: ApiConfig) -> ApiClient:
if cfg.kind == "rest":
if not cfg.base_url:
raise ValueError("base_url required for REST client")
return RestApiClient(base_url=cfg.base_url)
if cfg.kind == "graphql":
if not cfg.endpoint:
raise ValueError("endpoint required for GraphQL client")
return GraphqlApiClient(endpoint=cfg.endpoint)
return MockApiClient()
Taki układ pojawia się regularnie w narzędziach CLI, mikrousługach czy integracjach zewnętrznych API. W TypeScripcie odpowiednikiem bywa fabryka biorąca Config z pliku .json lub z otoczenia procesów (np. w aplikacjach Node).
Źródło: Pexels | Autor: ThisIsEngineering
Wzorzec 2 – Singleton bez magii: kiedy współdzielenie stanu ma sens
Po co w ogóle Singleton w Pythonie i TypeScripcie?
Singleton pojawia się wszędzie tam, gdzie jakiś obiekt:
jest kosztowny w utworzeniu (połączenie z bazą, ciężka konfiguracja),
powinien być współdzielony (cache, globalna konfiguracja, logger),
musi być spójny w ramach jednego procesu lub requestu.
Problem w praktyce zwykle nie brzmi „potrzebuję Singletona”, lecz raczej „mamy pięć instancji klienta bazy, każda z własnymi ustawieniami”. Singleton uporządkowuje taki bałagan, ale tylko wtedy, gdy jest użyty świadomie.
Antywzorzec: „magiczny” Singleton
Klasyczne, podręcznikowe implementacje z nadpisywaniem __new__ w Pythonie albo statycznym polem w TypeScripcie łatwo zamieniają się w globalny stan trudny do testowania.
class BadSingleton:
_instance: "BadSingleton | None" = None
def __new__(cls) -> "BadSingleton":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Kod wygląda efektownie, ale testy zaczynają odczuwać skutki: stan trudno zresetować, kolejność uruchamiania przypadków ma znaczenie. Z perspektywy projektowej to koszt, który nie zawsze się zwraca.
Prostsza droga w Pythonie: moduł jako Singleton
W Pythonie moduł jest ładowany raz na proces. To naturalny, prosty „Singleton bez klasy”.
# config.py
from functools import lru_cache
from pydantic import BaseSettings
class Settings(BaseSettings):
db_url: str
debug: bool = False
@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings() # wczytuje z env / pliku
W innym miejscu kodu:
from .config import get_settings
def handle_request():
settings = get_settings()
...
Fakty:
Settings tworzone jest leniwie przy pierwszym użyciu,
każde kolejne wywołanie zwraca tę samą instancję,
w testach lru_cache można wyczyścić (get_settings.cache_clear()) i wstrzykiwać inne źródła konfiguracji.
To praktyczny, idiomatyczny Singleton – bez specjalnych konstrukcji w klasie.
Singleton w TypeScripcie: moduł ES i statyczne instancje
W TypeScripcie (Node, bundlery) podobną rolę pełni moduł ES. Import jest cache'owany, więc eksportowana instancja klasy jest współdzielona.
// service.ts
import { settings } from "./config";
export async function handleRequest() {
if (settings.debug) {
console.log("Debug mode");
}
}
Nie ma tu „oficjalnej” etykiety Singleton, ale zachowanie jest identyczne: jedna instancja na proces, leniwe stworzenie przy pierwszym imporcie. Różnica względem Pythona jest taka, że w TypeScripcie łatwiej podmienić instancję przez DI (np. w NestJS) lub helper testowy.
Singleton per żądanie: kontekst aplikacji
W aplikacjach webowych częściej potrzebna jest jedna instancja na request niż na cały proces. Wtedy zamiast globalnego Singletona stosuje się coś w rodzaju „request-scoped Singleton”.
Przykład w FastAPI:
from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session
app = FastAPI()
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
# w obrębie jednego requestu `db` jest współdzielone
...
FastAPI pilnuje cyklu życia obiektu. Z punktu widzenia logiki biznesowej to Singleton w obrębie jednego żądania, ale pozbawiony globalnego stanu.
Framework utrzymuje jedną instancję serwisu na żądanie HTTP. Strategia jest ta sama: współdzielić kosztowne lub kontekstowe zasoby, ale zamknąć je w rozsądnym zakresie.
Kiedy unikać Singletona
Są sytuacje, w których współdzielony stan robi więcej szkody niż pożytku:
logika domenowa zależna od sekwencji wywołań (łatwo o błędy w konkurencji),
testy równoległe (globalny stan utrudnia izolację),
wieloprocesowe aplikacje (Singleton w jednym procesie nie widzi danych z innych).
Jeśli obiekt często zmienia stan i zależy od danych użytkownika, bezpieczniejszy bywa wstrzykiwany serwis bez globalnego współdzielenia. Singleton sprawdza się lepiej przy stanie konfiguracyjnym i zasobach typu cache, a gorzej przy zasadach biznesowych.
Wzorzec 3 – Strategy: zamiana if-ów na wymienne algorytmy
Strategia jako odpowiedź na rozrastające się warunki
Do Strategii prowadzi zwykle obserwacja jednego zjawiska: ta sama operacja jest wykonywana na różne sposoby, a wybór konkretnego wariantu opiera się na parametrach, typie klienta, flagach. Kod zaczyna gubić się w if/elif, a pojawienie się nowego przypadku wymaga modyfikacji starej funkcji.
Strategia to prosta struktura:
interfejs „co trzeba zrobić” (np. calculate_price),
zestaw implementacji „jak to zrobić”,
mechanizm wyboru odpowiedniej strategii.
Strategia w Pythonie: od klas do funkcji i słowników
Klasyczna wersja z hierarchią klas w Pythonie bywa zbyt ciężka. Często wystarcza lekki interfejs oparty na protokole i funkcjach.
każda strategia to zwykła funkcja spełniająca protokół,
słownik pełni rolę prostego rejestru Strategii,
dodanie nowego wariantu sprowadza się do dopisania funkcji i wpisu do STRATEGIES.
Taka struktura pojawia się w systemach rabatów, walidacji, generowania raportów – wszędzie tam, gdzie „rodzaj” operacji wybierany jest na podstawie danych wejściowych.
Strategia w TypeScripcie: interfejs lub typ funkcji
W TypeScripcie wybór często pada na typ funkcji, bo integruje się dobrze z systemem typów.
kompilator nie pozwoli wywołać calculatePrice z nieznanym rodzajem strategii,
nowy rodzaj wymaga dopisania do typu unii i do obiektu STRATEGIES,
brak ręcznego if/else zmniejsza ryzyko pomyłek i „zapomnianych” przypadków.
Strategia zagnieżdżona w obiektach domenowych
Często Strategia ląduje bezpośrednio w obiekcie domenowym, a nie w oddzielnej funkcji. Przykład z praktyki: aplikacja do rozliczania opłat, gdzie różne typy produktów mają różne algorytmy ustalania marży.
from dataclasses import dataclass
from typing import Callable
MarginStrategy = Callable[[float], float]
@dataclass
class Product:
name: str
base_price: float
margin_strategy: MarginStrategy
def final_price(self) -> float:
return self.margin_strategy(self.base_price)
def high_margin(base: float) -> float:
return base * 1.5
def low_margin(base: float) -> float:
return base * 1.1
Strategia jest tu parametrem produktu. Kod nie musi już nigdzie indziej pytać „jaki to typ produktu?”, bo obiekt niesie ze sobą sposób obliczania ceny.
Strategia w aplikacji frontowej: TypeScript + React
W frontendzie Strategia często przyjmuje postać mapy sposobów renderowania lub walidowania. Np. różne typy pól formularza:
Strategia w React: mapa komponentów zamiast if-ów
Frontendy szybko pokazują, czy Strategia została dobrze zastosowana. Przypadek typowy: wiele typów pól formularza i rosnący komponent z warunkami renderowania.
Z perspektywy biznesu oznacza to jedną rzecz: zmiana sposobu liczenia kosztów wysyłki może odbyć się bez wdrażania nowej wersji aplikacji – wystarczy zmodyfikować konfigurację. Kod jest przygotowany na ten ruch, bo Strategia została wydzielona.
Wzorzec 4 – Adapter: godzenie starych i nowych interfejsów
Skąd bierze się potrzeba Adaptera
Najczęstszy scenariusz jest prozaiczny: istniejący kod używa jakiegoś interfejsu, a nowa biblioteka albo serwis dostarcza podobną funkcjonalność, lecz pod innym API. Przepisywanie całej aplikacji do nowego interfejsu jest kosztowne, więc powstaje warstwa pośrednia – Adapter.
Konstrukcja jest powtarzalna:
jest oczekiwany interfejs (np. PaymentGateway), którego używa reszta systemu,
jest zastępcza lub nowa implementacja o innym kształcie API,
Adapter owija nową implementację i „tłumaczy” wywołania na oczekiwany kształt.
Co wiemy? Adapter nie dodaje funkcji biznesowej – jego rolą jest korekta kształtu danych i sygnatur metod.
Adapter w Pythonie: stare API spotyka nowe SDK
Przykład z migracji bramki płatności. Stary kod opiera się na interfejsie:
from typing import Protocol
class PaymentGateway(Protocol):
def charge(self, amount: int, currency: str, token: str) -> str:
"""Zwraca identyfikator transakcji lub rzuca wyjątek."""
...
Nowe SDK dostawcy wygląda inaczej:
# nowy_sdk.py
class NewPaymentClient:
def create_payment(self, payload: dict) -> dict:
# wysyła żądanie HTTP, zwraca dane JSON
...
Reszta aplikacji nadal woła gateway.charge(...). Różnice w parametrach, formacie odpowiedzi i obsłudze błędów ukrywa Adapter. W testach można podstawiać atrapę PaymentGateway bez konieczności dotykania SDK.
Adapter w TypeScripcie: integracja z zewnętrznym API
W ekosystemie TypeScriptu Adapter często siedzi na granicy z backendem HTTP. Załóżmy, że istnieje interfejs klienta użytkowników:
komponenty w React/Next korzystają z typowanego UsersClient,
zmiany w strukturze odpowiedzi HTTP dotykają tylko Adaptera,
testy mogą podmienić UsersClient na fałszywą implementację bez warstwy HTTP.
To typowy obrazek z projektów migracyjnych: stare API frontendowe zostaje, natomiast pod spodem podmieniany jest sposób jego realizacji.
Adapter jako „tłumacz” pomiędzy warstwami
Adapter nie musi dotyczyć tylko zewnętrznych bibliotek. Równie często pełni rolę tłumacza między warstwą domenową a bazą danych czy modelem ORM.
from dataclasses import dataclass
@dataclass
class User:
id: int
email: str
is_active: bool
# uproszczony model ORM (np. SQLAlchemy)
class UserModel:
id: int
email_address: str
active: bool
class UserRepository:
def __init__(self, session):
self._session = session
def get(self, user_id: int) -> User | None:
row = self._session.query(UserModel).get(user_id)
if row is None:
return None
return User(id=row.id, email=row.email_address, is_active=row.active)
def save(self, user: User) -> None:
row = self._session.query(UserModel).get(user.id) or UserModel()
row.email_address = user.email
row.active = user.is_active
self._session.add(row)
self._session.commit()
Repozytorium działa tu jako Adapter między obiektem domenowym User a modelem ORM. Komunikat jest prosty: logika biznesowa sięga po spójną reprezentację użytkownika, szczegóły bazy danych zostają w warstwie dostępu do danych.
Adapter funkcyjny: drobna korekta bez klas
Nie każdy Adapter musi być klasą. Prosta funkcja mapująca też pełni tę rolę, szczególnie w TypeScripcie.
interface LegacyOrder {
id: string;
total: number;
created_at: string; // ISO
}
interface Order {
id: string;
totalCents: number;
createdAt: Date;
}
export function adaptLegacyOrder(input: LegacyOrder): Order {
return {
id: input.id,
totalCents: Math.round(input.total * 100),
createdAt: new Date(input.created_at),
};
}
Takie małe Adaptery porządkują granice między „światem zewnętrznym” (API, stare moduły) a „światem wewnętrznym” (typy domenowe). W codziennej pracy pojawiają się przy migracjach schematu, refaktoringach modeli oraz integracjach z systemami partnerskimi.
Adapter a testowanie: kontrolowany punkt styku
Na poziomie testów Adapter staje się naturalnym miejscem, w którym symuluje się zachowanie zewnętrznych zależności. Zamiast stubować surowe wywołania HTTP czy metod SDK, podmienia się cały Adapter.
Punkt styku z zewnętrznym światem jest jeden – interfejs PaymentGateway. To on reprezentuje oczekiwany kontrakt, pozostawiając implementacji (Adapterowi) swobodę w korzystaniu z konkretnej biblioteki.
Najczęściej zadawane pytania (FAQ)
Po co mi wzorce projektowe, skoro w Pythonie i TypeScripcie mogę „po prostu pisać kod”?
Wzorce projektowe przede wszystkim dają wspólny język w zespole. Łatwiej powiedzieć „zróbmy tu Strategię” niż przez 10 minut tłumaczyć, że chodzi o rozbicie 12 if-ów wybierających algorytm liczenia ceny. Przy mieszanym składzie (Python backend, TypeScript frontend) takie skróty mocno przyspieszają rozmowę.
Same definicje niewiele zmieniają. Zysk pojawia się wtedy, gdy umiesz powiązać nazwę wzorca z konkretnym problemem w kodzie: rosnącą klasą‑„bąblem”, duplikacją warunków, trudnym w testowaniu modułem. Wtedy wzorzec staje się narzędziem, a nie teorią.
Kiedy warto stosować wzorce projektowe w projektach Python + TypeScript?
Wzorce zaczynają być przydatne, gdy projekt rośnie: przybywa podobnych, ale nieidentycznych fragmentów logiki, kolejne usługi dotykają tych samych modułów, a zależności pomiędzy komponentami robią się niejasne. Typowy sygnał to moment, w którym nowa funkcja wymaga przeklikania się przez kilka plików, żeby zrozumieć przepływ danych.
Drugi częsty przypadek to praca wielu zespołów na jednym kodzie. Jasno nazwane wzorce (Adapter, Facade, Strategy) porządkują strukturę i ułatwiają onboarding nowych osób. Pytanie kontrolne, które warto sobie zadać: czy problem zrozumienia kodu wynika z brakującej nazwy/struktury, czy z samej logiki biznesowej?
Czy klasyczne wzorce GoF mają sens w nowoczesnym Pythonie i TypeScripcie?
Część wzorców GoF została wchłonięta przez sam język i frameworki. W Pythonie dekoratory realizują wiele zastosowań Decoratora, moduły zachowują się jak naturalne Singletony, a funkcje wyższego rzędu pokrywają sporą część Strategii. W TypeScripcie interfejsy, typy unii i klasy uprościły wiele dawnych, rozbudowanych hierarchii.
Wciąż aktualne pozostają zwłaszcza wzorce strukturalne (Adapter, Facade) i te, które pomagają poradzić sobie z zależnościami między modułami. To one dobrze pasują do świata mikroserwisów, API i podziału na backend w Pythonie oraz frontend/Node w TypeScripcie.
Jak inaczej stosuje się wzorce projektowe w Pythonie, a inaczej w TypeScripcie?
Python skłania do rozwiązań opartych na dynamice i duck-typingu. Strategia może być zwykłą funkcją, obiektem spełniającym protokół albo wpisem w słowniku strategii. Często nie trzeba definicji klasy – wystarczy uzgodniony „kształt” parametru lub modułu.
W TypeScripcie ten sam pomysł zwykle opisuje się interfejsem lub typem funkcji, a kompilator pilnuje zgodności implementacji. Strategia staje się np. interface PricingStrategy albo type PricingStrategy = (input: ...) => number. Co wiemy dzięki temu? Łatwiej złapać niezgodności na etapie kompilacji i lepiej współpracować z IDE.
Jak rozpoznać, że w projekcie brakuje wzorca, a nie po prostu refaktoryzacji?
Pierwszy krok to zauważenie „code smells”: powtarzające się bloki if/else, klasy, które „robią wszystko”, funkcje mające po kilkaset linii, trudne do sensownego przetestowania moduły. To symptomy, a nie diagnoza. Kolejne pytanie brzmi: czy ten problem przypomina typowy przypadek, dla którego opisano konkretny wzorzec?
Jeśli odpowiedź jest twierdząca („ciągle dokładam if-y do wyboru algorytmu” → Strategy, „wszędzie wisi klient konkretnej biblioteki” → Adapter/Facade), wtedy wzorzec daje gotowy szkielet refaktoryzacji. Jeśli nie – często wystarczy prostsze uporządkowanie funkcji, modułów i nazw, bez wyciągania „ciężkiej artylerii” w postaci wzorców.
Czy trzeba ręcznie implementować wzorce, jeśli używam Django, FastAPI, Reacta czy NestJS?
Wiele frameworków ma wzorce wbudowane. Django korzysta z Template Method w generic views i z fabryk w ORM, FastAPI udostępnia prosty mechanizm dependency injection funkcjami, NestJS jest zbudowany wokół DI, modułów i serwisów. W takich środowiskach dodatkowe, własne hierarchie fabryk czy kontenery DI często tylko dublują istniejące rozwiązania.
Praktyczna zasada: najpierw sprawdź, czy problemu nie da się rozwiązać idiomem lub mechanizmem frameworka. Dopiero gdy brak pasującego narzędzia, warto sięgnąć po klasyczny opis wzorca i dostosować go do konkretnego języka i stosu technologicznego.
Jak „tłumaczyć” opis wzorca na realny kod w projekcie?
Punktem wyjścia jest zawsze problem, nie diagram UML. Najpierw nazwij sytuację, która boli (np. trudne testowanie przez sztywne zależności od zewnętrznego API). Potem sprawdź, jakie „siły” działają w projekcie: stabilność API, tempo zmian biznesowych, ograniczenia technologiczne.
Dopiero potem wybierz wzorzec i przełóż go na idiomy języka: w Pythonie może to być moduł z funkcjami‑strategiami lub prosty słownik strategii, w TypeScripcie – interfejs plus klasy lub typ funkcji. Kluczowe pytanie kontrolne: czy po wprowadzeniu wzorca kod stał się bardziej zrozumiały dla innych osób w zespole? Jeśli nie, implementacja wzorca jest prawdopodobnie zbyt „książkowa” albo nieadekwatna do kontekstu.