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. Single