Wzorce projektowe w Pythonie dla czystej architektury
Twórz utrwalane aplikacje Pythona z wykorzystaniem wzorców projektowych SOLID
Clean Architecture przekształciła sposób, w jaki programiści tworzą skalowalne, utrzymywalne aplikacje, podkreślając oddzielenie obowiązków i zarządzanie zależnościami.
W Pythonie te zasady łączą się z dynamiczną naturą języka, tworząc elastyczne, testowalne systemy, które ewoluują wraz z wymaganiami biznesowymi bez stania się długiem technicznym.

Zrozumienie Clean Architecture w Pythonie
Clean Architecture, wprowadzona przez Roberta C. Martina (Uncle Boba), organizuje oprogramowanie w koncentryczne warstwy, w których zależności wskazują wewnętrznie w stronę rdzennej logiki biznesowej. Ten wzorzec architektoniczny zapewnia, że kluczowe reguły biznesowe aplikacji pozostają niezależne od frameworków, baz danych i usług zewnętrznych.
Podstawowa filozofia
Podstawowy zasada jest prosta, ale potężna: logika biznesowa nie powinna zależeć od infrastruktury. Twoje obiekty domeny, przypadki użycia i reguły biznesowe powinny działać niezależnie od tego, czy korzystasz z PostgreSQL czy MongoDB, FastAPI czy Flask, AWS czy Azure.
W Pythonie ta filozofia idealnie pasuje do natury języka “duck typing” i programowania opartego na protokołach, umożliwiając czyste oddzielenie bez ceremonii wymaganej w językach statycznie typowanych.
Cztery warstwy Clean Architecture
Warstwa obiektów (Domena): Puri czyste obiekty biznesowe z regułami biznesowymi obowiązującymi w całym przedsiębiorstwie. Są to POJO (Plain Old Python Objects) bez żadnych zależności zewnętrznych.
Warstwa przypadków użycia (Aplikacja): Reguły biznesowe aplikacji, które koordynują przepływ danych między obiektami a usługami zewnętrznymi.
Warstwa adapterów interfejsów: Konwertuje dane w formacie najbardziej wygodnym dla przypadków użycia i obiektów, oraz w formacie wymaganym przez agencje zewnętrzne.
Warstwa frameworków i sterowników: Wszystkie szczegóły zewnętrzne, takie jak bazy danych, frameworki sieciowe i zewnętrzne API.
Zasady SOLID w Pythonie
Zasady SOLID stanowią fundament czystej architektury. Przeanalizujmy, jak każda z nich manifestuje się w Pythonie. Dla pełnego przeglądu wzorców projektowych w Pythonie zobacz Python Design Patterns Guide.
Zasada jednej odpowiedzialności (SRP)
Każda klasa powinna mieć jeden powód do zmiany:
# Zły: Wiele odpowiedzialności
class UserManager:
def create_user(self, user_data):
# Tworzenie użytkownika
pass
def send_welcome_email(self, user):
# Wysyłanie e-maila
pass
def log_creation(self, user):
# Logowanie do pliku
pass
# Dobry: Oddzielone odpowiedzialności
class UserService:
def __init__(self, repository, email_service, logger):
self.repository = repository
self.email_service = email_service
self.logger = logger
def create_user(self, user_data):
user = User(**user_data)
self.repository.save(user)
self.email_service.send_welcome(user)
self.logger.info(f"Użytkownik utworzony: {user.id}")
return user
Zasada otwartej/ zamkniętej (OCP)
Jednostki oprogramowania powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację:
from abc import ABC, abstractmethod
from typing import Protocol
# Używanie Protokołu (Python 3.8+)
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Logika karty kredytowej
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Logika PayPal
return True
# Łatwe rozszerzanie bez modyfikacji istniejącego kodu
class CryptoProcessor:
def process_payment(self, amount: float) -> bool:
# Logika kryptowalut
return True
Zasada podstawienia Liskova (LSP)
Obiekty powinny być zamieniane na ich podtypy bez naruszania działania programu:
from abc import ABC, abstractmethod
class DataStore(ABC):
@abstractmethod
def save(self, key: str, value: str) -> None:
pass
@abstractmethod
def get(self, key: str) -> str:
pass
class PostgreSQLStore(DataStore):
def save(self, key: str, value: str) -> None:
# Implementacja PostgreSQL
pass
def get(self, key: str) -> str:
# Implementacja PostgreSQL
return ""
class RedisStore(DataStore):
def save(self, key: str, value: str) -> None:
# Implementacja Redis
pass
def get(self, key: str) -> str:
# Implementacja Redis
return ""
# Oba mogą być używane zamiennie
def process_data(store: DataStore, key: str, value: str):
store.save(key, value)
return store.get(key)
Zasada segregacji interfejsów (ISP)
Klienci nie powinni być zmuszeni do zależności od interfejsów, których nie używają:
# Zły: Gruby interfejs
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep(self): pass
# Dobry: Oddzielone interfejsy
class Workable(Protocol):
def work(self) -> None: ...
class Eatable(Protocol):
def eat(self) -> None: ...
class Human:
def work(self) -> None:
print("Pracuję")
def eat(self) -> None:
print("Jem")
class Robot:
def work(self) -> None:
print("Pracuję")
# Nie potrzebna metoda eat
Zasada odwrócenia zależności (DIP)
Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji:
from typing import Protocol
# Abstrakcja
class EmailSender(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
# Moduł niskiego poziomu
class SMTPEmailSender:
def send(self, to: str, subject: str, body: str) -> None:
# Implementacja SMTP
pass
# Moduł wysokiego poziomu zależny od abstrakcji
class UserRegistrationService:
def __init__(self, email_sender: EmailSender):
self.email_sender = email_sender
def register(self, email: str, name: str):
# Logika rejestracji
self.email_sender.send(
to=email,
subject="Witaj!",
body=f"Witaj {name}"
)
Wzorzec Repository: Abstrahowanie dostępu do danych
Wzorzec Repository oferuje interfejs podobny do kolekcji do dostępu do obiektów domeny, ukrywając szczegóły przechowywania danych.
Podstawowa implementacja Repository
from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
from uuid import UUID, uuid4
@dataclass
class User:
id: UUID
email: str
name: str
is_active: bool = True
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> User:
pass
@abstractmethod
def get_by_id(self, user_id: UUID) -> Optional[User]:
pass
@abstractmethod
def get_by_email(self, email: str) -> Optional[User]:
pass
@abstractmethod
def list_all(self) -> List[User]:
pass
@abstractmethod
def delete(self, user_id: UUID) -> bool:
pass
Implementacja SQLAlchemy
from sqlalchemy import create_engine, Column, String, Boolean
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
Base = declarative_base()
class UserModel(Base):
__tablename__ = 'users'
id = Column(PGUUID(as_uuid=True), primary_key=True)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
class SQLAlchemyUserRepository(UserRepository):
def __init__(self, session: Session):
self.session = session
def save(self, user: User) -> User:
user_model = UserModel(
id=user.id,
email=user.email,
name=user.name,
is_active=user.is_active
)
self.session.add(user_model)
self.session.commit()
return user
def get_by_id(self, user_id: UUID) -> Optional[User]:
user_model = self.session.query(UserModel).filter(
UserModel.id == user_id
).first()
if not user_model:
return None
return User(
id=user_model.id,
email=user_model.email,
name=user_model.name,
is_active=user_model.is_active
)
def get_by_email(self, email: str) -> Optional[User]:
user_model = self.session.query(UserModel).filter(
UserModel.email == email
).first()
if not user_model:
return None
return User(
id=user_model.id,
email=user_model.email,
name=user_model.name,
is_active=user_model.is_active
)
def list_all(self) -> List[User]:
users = self.session.query(UserModel).all()
return [
User(
id=u.id,
email=u.email,
name=u.name,
is_active=u.is_active
)
for u in users
]
def delete(self, user_id: UUID) -> bool:
result = self.session.query(UserModel).filter(
UserModel.id == user_id
).delete()
self.session.commit()
return result > 0
Repository w pamięci dla testów
class InMemoryUserRepository(UserRepository):
def __init__(self):
self.users: dict[UUID, User] = {}
def save(self, user: User) -> User:
self.users[user.id] = user
return user
def get_by_id(self, user_id: UUID) -> Optional[User]:
return self.users.get(user_id)
def get_by_email(self, email: str) -> Optional[User]:
for user in self.users.values():
if user.email == email:
return user
return None
def list_all(self) -> List[User]:
return list(self.users.values())
def delete(self, user_id: UUID) -> bool:
if user_id in self.users:
del self.users[user_id]
return True
return False
Warstwa usług: Koordynowanie logiki biznesowej
Warstwa usług implementuje przypadki użycia i koordynuje przepływ między repozytoriami, usługami zewnętrznymi i logiką domeny.
from typing import Optional
from uuid import uuid4
class UserAlreadyExistsError(Exception):
pass
class UserNotFoundError(Exception):
pass
class UserService:
def __init__(
self,
user_repository: UserRepository,
email_service: EmailSender,
event_publisher: 'EventPublisher'
):
self.user_repository = user_repository
self.email_service = email_service
self.event_publisher = event_publisher
def register_user(self, email: str, name: str) -> User:
# Sprawdzenie, czy użytkownik istnieje
existing_user = self.user_repository.get_by_email(email)
if existing_user:
raise UserAlreadyExistsError(f"Użytkownik z e-mailem {email} już istnieje")
# Utworzenie nowego użytkownika
user = User(
id=uuid4(),
email=email,
name=name,
is_active=True
)
# Zapisanie w repozytorium
user = self.user_repository.save(user)
# Wysłanie powitalnego e-maila
self.email_service.send(
to=user.email,
subject="Witaj!",
body=f"Witaj {user.name}, witaj na naszej platformie!"
)
# Wyślij zdarzenie
self.event_publisher.publish('user.registered', {
'user_id': str(user.id),
'email': user.email
})
return user
def deactivate_user(self, user_id: UUID) -> User:
user = self.user_repository.get_by_id(user_id)
if not user:
raise UserNotFoundError(f"Użytkownik {user_id} nie znaleziony")
user.is_active = False
user = self.user_repository.save(user)
self.event_publisher.publish('user.deactivated', {
'user_id': str(user.id)
})
return user
Iniekcja zależności w Pythonie
Dynamiczna natura Pythona sprawia, że iniekcja zależności jest prosta bez konieczności korzystania z ciężkich frameworków.
Iniekcja przez konstruktor
class OrderService:
def __init__(
self,
order_repository: 'OrderRepository',
payment_processor: PaymentProcessor,
notification_service: 'NotificationService'
):
self.order_repository = order_repository
self.payment_processor = payment_processor
self.notification_service = notification_service
def place_order(self, order_data: dict):
# Użycie wstrzykiwanych zależności
pass
Prosty kontener zależności
from typing import Dict, Type, Callable, Any
class Container:
def __init__(self):
self._services: Dict[Type, Callable] = {}
self._singletons: Dict[Type, Any] = {}
def register(self, interface: Type, factory: Callable):
self._services[interface] = factory
def register_singleton(self, interface: Type, instance: Any):
self._singletons[interface] = instance
def resolve(self, interface: Type):
if interface in self._singletons:
return self._singletons[interface]
factory = self._services.get(interface)
if factory:
return factory(self)
raise ValueError(f"Nie znaleziono rejestracji dla {interface}")
# Użycie
def create_container() -> Container:
container = Container()
# Rejestrowanie usług
container.register_singleton(
Session,
sessionmaker(bind=create_engine('postgresql://...'))()
)
container.register(
UserRepository,
lambda c: SQLAlchemyUserRepository(c.resolve(Session))
)
container.register(
EmailSender,
lambda c: SMTPEmailSender()
)
container.register(
UserService,
lambda c: UserService(
c.resolve(UserRepository),
c.resolve(EmailSender),
c.resolve(EventPublisher)
)
)
return container
Architektura Sześciokątna (Porty i Adaptery)
Architektura Sześciokątna umieszcza logikę biznesową w centrum, a adaptery zajmują się komunikacją zewnętrzną.
Definiowanie portów (interfejsów)
# Wejściowy port (główny)
class CreateUserUseCase(Protocol):
def execute(self, request: 'CreateUserRequest') -> 'CreateUserResponse':
...
# Wyjściowy port (pomocniczy)
class UserPersistencePort(Protocol):
def save(self, user: User) -> User:
...
def find_by_email(self, email: str) -> Optional[User]:
...
Implementacja adapterów
from pydantic import BaseModel, EmailStr
# Wejściowy adapter (API REST)
from fastapi import FastAPI, Depends, HTTPException
class CreateUserRequest(BaseModel):
email: EmailStr
name: str
class CreateUserResponse(BaseModel):
id: str
email: str
name: str
app = FastAPI()
@app.post("/users", response_model=CreateUserResponse)
def create_user(
request: CreateUserRequest,
user_service: UserService = Depends(get_user_service)
):
try:
user = user_service.register_user(
email=request.email,
name=request.name
)
return CreateUserResponse(
id=str(user.id),
email=user.email,
name=user.name
)
except UserAlreadyExistsError as e:
raise HTTPException(status_code=400, detail=str(e))
# Wyjściowy adapter (baza danych)
# Już zaimplementowany jako SQLAlchemyUserRepository
Wzorce projektowe oparte na domenie
Obiekty wartości
Niezmienne obiekty zdefiniowane przez swoje atrybuty:
from dataclasses import dataclass
from typing import Pattern
import re
@dataclass(frozen=True)
class Email:
value: str
EMAIL_PATTERN: Pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
def __post_init__(self):
if not self.EMAIL_PATTERN.match(self.value):
raise ValueError(f"Nieprawidłowy e-mail: {self.value}")
def __str__(self):
return self.value
@dataclass(frozen=True)
class Money:
amount: float
currency: str
def __post_init__(self):
if self.amount < 0:
raise ValueError("Kwota nie może być ujemna")
if self.currency not in ['USD', 'EUR', 'GBP']:
raise ValueError(f"Nieobsługiwana waluta: {self.currency}")
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError("Nie można dodać różnych walut")
return Money(self.amount + other.amount, self.currency)
Agregaty
Klaster obiektów domeny traktowanych jako jednostka:
from dataclasses import dataclass, field
from typing import List
from datetime import datetime
@dataclass
class OrderItem:
product_id: UUID
quantity: int
price: Money
def total(self) -> Money:
return Money(
self.price.amount * self.quantity,
self.price.currency
)
@dataclass
class Order:
id: UUID
customer_id: UUID
items: List[OrderItem] = field(default_factory=list)
status: str = "pending"
created_at: datetime = field(default_factory=datetime.now)
def add_item(self, product_id: UUID, quantity: int, price: Money):
item = OrderItem(product_id, quantity, price)
self.items.append(item)
def remove_item(self, product_id: UUID):
self.items = [
item for item in self.items
if item.product_id != product_id
]
def total(self) -> Money:
if not self.items:
return Money(0, "USD")
return sum(
(item.total() for item in self.items),
Money(0, self.items[0].price.currency)
)
def confirm(self):
if not self.items:
raise ValueError("Nie można potwierdzić pustego zamówienia")
if self.status != "pending":
raise ValueError("Zamówienie już przetworzone")
self.status = "confirmed"
Zdarzenia domeny
Zdarzenia domeny umożliwiają luźne sprzężenie między komponentami i wspierają architektury oparte na zdarzeniach. Dla systemów opartych na zdarzeniach w dużych skalach rozważ implementację strumieni zdarzeń z usługami takimi jak AWS Kinesis — zobacz Wbudowanie mikroserwisów opartych na zdarzeniach z użyciem AWS Kinesis dla szczegółowego przewodnika.
from dataclasses import dataclass
from datetime import datetime
from typing import List, Callable
@dataclass
class DomainEvent:
occurred_at: datetime = field(default_factory=datetime.now)
@dataclass
class OrderConfirmed(DomainEvent):
order_id: UUID
customer_id: UUID
total: Money
class EventPublisher:
def __init__(self):
self._handlers: Dict[Type, List[Callable]] = {}
def subscribe(self, event_type: Type, handler: Callable):
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def publish(self, event: DomainEvent):
event_type = type(event)
handlers = self._handlers.get(event_type, [])
for handler in handlers:
handler(event)
Nowoczesne funkcje Pythona dla czystej architektury
Nowoczesne funkcje Pythona sprawiają, że implementowanie czystej architektury jest bardziej eleganckie i typowo bezpieczne. Jeśli potrzebujesz szybkiego odniesienia do składni i funkcji Pythona, sprawdź Python Cheatsheet.
Wskazówki typowe i protokoły
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict:
...
@classmethod
def from_dict(cls, data: dict) -> 'Serializable':
...
def serialize(obj: Serializable) -> dict:
return obj.to_dict()
Pydantic do walidacji
from pydantic import BaseModel, Field, validator
from typing import Optional
class CreateUserDTO(BaseModel):
email: EmailStr
name: str = Field(..., min_length=2, max_length=100)
age: Optional[int] = Field(None, ge=0, le=150)
@validator('name')
def name_must_not_contain_numbers(cls, v):
if any(char.isdigit() for char in v):
raise ValueError('Imię nie może zawierać cyfr')
return v
class Config:
frozen = True # Ustawienie jako niemutowalne
Async/Await dla operacji we/wy
Składnia async/await w Pythonie jest szczególnie potężna w przypadku operacji we/wy w czystej architekturze, umożliwiając niesynchronizowane interakcje z bazami danych i zewnętrznymi usługami. Podczas wdrażania aplikacji Pythona na platformach bezserwerowych, zrozumienie cech wydajności staje się kluczowe – zobacz AWS lambda performance: JavaScript vs Python vs Golang dla wskazówek dotyczących optymalizacji funkcji Pythona w architekturze bezserwerowej.
from typing import List
import asyncio
class AsyncUserRepository(ABC):
@abstractmethod
async def save(self, user: User) -> User:
pass
@abstractmethod
async def get_by_id(self, user_id: UUID) -> Optional[User]:
pass
class AsyncUserService:
def __init__(self, repository: AsyncUserRepository):
self.repository = repository
async def register_user(self, email: str, name: str) -> User:
user = User(id=uuid4(), email=email, name=name)
return await self.repository.save(user)
async def get_users_batch(self, user_ids: List[UUID]) -> List[User]:
tasks = [self.repository.get_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
Najlepsze praktyki dotyczące struktury projektu
Poprawna organizacja projektu jest kluczowa dla utrzymania czystej architektury. Przed ustawieniem struktury projektu upewnij się, że korzystasz z wirtualnych środowisk Pythona do izolacji zależności. venv Cheatsheet zawiera wszystko, co musisz wiedzieć na temat zarządzania środowiskami wirtualnymi. Dla nowoczesnych projektów Pythona rozważ użycie uv - Nowy menedżer pakietów, projektów i środowisk Pythona, który oferuje szybsze zarządzanie pakietami i konfigurację projektu.
moja_aplikacja/
├── domena/ # Reguły biznesowe firmy
│ ├── __init__.py
│ ├── encje/
│ │ ├── __init__.py
│ │ ├── użytkownik.py
│ │ └── zamówienie.py
│ ├── wartości/
│ │ ├── __init__.py
│ │ ├── e-mail.py
│ │ └── pieniądze.py
│ ├── zdarzenia/
│ │ ├── __init__.py
│ │ └── zdarzenia_użytkownika.py
│ └── wyjątki.py
│
├── aplikacja/ # Reguły biznesowe aplikacji
│ ├── __init__.py
│ ├── przypadki_użycia/
│ │ ├── __init__.py
│ │ ├── utwórz_użytkownika.py
│ │ └── złożenie_zamówienia.py
│ ├── usługi/
│ │ ├── __init__.py
│ │ └── usługa_użytkownika.py
│ └── porty/
│ ├── __init__.py
│ ├── repozytoria.py
│ └── usługi_zewnętrzne.py
│
├── infrastruktura/ # Interfejsy zewnętrzne
│ ├── __init__.py
│ ├── trwałość/
│ │ ├── __init__.py
│ │ ├── sqlalchemy/
│ │ │ ├── modele.py
│ │ │ └── repozytoria.py
│ │ └── mongodb/
│ │ └── repozytoria.py
│ ├── komunikacja/
│ │ ├── __init__.py
│ │ └── publisher_rabbitmq.py
│ ├── usługi_zewnętrzne/
│ │ ├── __init__.py
│ │ └── usługa_e-mail.py
│ └── konfiguracja.py
│
├── prezentacja/ # Warstwa UI/API
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── zależności.py
│ │ ├── trasy/
│ │ │ ├── __init__.py
│ │ │ ├── użytkownicy.py
│ │ │ └── zamówienia.py
│ │ └── schematy/
│ │ ├── __init__.py
│ │ └── schematy_użytkownika.py
│ └── cli/
│ └── polecenia.py
│
├── testy/
│ ├── testy_jednostkowe/
│ ├── testy_integracyjne/
│ └── testy_e2e/
│
├── main.py # Punkt wejścia aplikacji
├── kontener.py # Konfiguracja iniekcji zależności
├── pyproject.toml
└── README.md
Testowanie czystej architektury
Testowanie logiki domeny
import pytest
from uuid import uuid4
def test_utworzenie_użytkownika():
użytkownik = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
assert użytkownik.email == "test@example.com"
assert użytkownik.is_active is True
def test_obliczenie_sumy_zamówienia():
zamówienie = Order(id=uuid4(), customer_id=uuid4())
zamówienie.add_item(
uuid4(),
quantity=2,
price=Money(10.0, "USD")
)
zamówienie.add_item(
uuid4(),
quantity=1,
price=Money(5.0, "USD")
)
assert zamówienie.total().amount == 25.0
Testowanie integracji z repozytorium
@pytest.fixture
def repozytorium_w_pamieci():
return InMemoryUserRepository()
def test_zapisanie_i_pobranie_użytkownika(repozytorium_w_pamieci):
użytkownik = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
zapisany_użytkownik = repozytorium_w_pamieci.save(użytkownik)
pobrany_użytkownik = repozytorium_w_pamieci.get_by_id(użytkownik.id)
assert pobrany_użytkownik is not None
assert pobrany_użytkownik.email == użytkownik.email
Testowanie warstwy usług
from unittest.mock import Mock
def test_rejestracja_użytkownika():
# Przygotowanie
mock_repozytorium = Mock(spec=UserRepository)
mock_repozytorium.get_by_email.return_value = None
mock_repozytorium.save.return_value = User(
id=uuid4(),
email="test@example.com",
name="Test"
)
mock_email = Mock(spec=EmailSender)
mock_zdarzenia = Mock(spec=EventPublisher)
usługa = UserService(mock_repozytorium, mock_email, mock_zdarzenia)
# Wykonanie
użytkownik = usługa.register_user("test@example.com", "Test")
# Sprawdzenie
assert użytkownik.email == "test@example.com"
mock_repozytorium.save.assert_called_once()
mock_email.send.assert_called_once()
mock_zdarzenia.publish.assert_called_once()
Typowe pułapki i sposób ich unikania
Nadmierny projekt
Nie implementuj czystej architektury dla prostych aplikacji CRUD. Zacznij od prostego rozwiązania i refaktoryzuj, gdy złożoność rośnie.
Wyciekające abstrakcje
Upewnij się, że obiekty domeny nie zawierają adnotacji do bazy danych ani kodu specyficznych dla frameworków:
# Zły
from sqlalchemy import Column
@dataclass
class User:
id: Column(Integer, primary_key=True) # Framework wyciekający do domeny
# Dobry
@dataclass
class User:
id: UUID # Puro obiekt domeny
Zależności cykliczne
Używaj iniekcji zależności i interfejsów, aby przerwać zależności cykliczne między warstwami.
Ignorowanie kontekstu
Czysta architektura nie jest jednoznaczna dla wszystkich. Dostosuj ściśleść warstw w zależności od rozmiaru projektu i doświadczenia zespołu.
Przydatne linki
- Czysta architektura przez Roberta C. Martina
- Dokumentacja typów Pythona
- Dokumentacja Pydantic
- Oficjalne dokumenty FastAPI
- Dokumentacja ORM SQLAlchemy
- Biblioteka Dependency Injector
- Referencja do Domain-Driven Design
- Architektura z Pythonem
- Blog Martina Fowlera na temat architektury
- Przewodnik po wzorcach projektowych w Pythonie
- Python Cheatsheet
- venv Cheatsheet
- uv - Nowy menedżer pakietów, projektów i środowisk Pythona
- Wydajność AWS Lambda: JavaScript vs Python vs Golang
- Tworzenie mikroserwisów opartych na zdarzeniach z użyciem AWS Kinesis