Шаблоны проектирования Python для чистой архитектуры
Создавайте поддерживаемые приложения на Python с помощью принципов SOLID
Чистая архитектура революционизировала подход разработчиков к созданию масштабируемых, поддерживаемых приложений, делая акцент на разделение ответственности и управлении зависимостями.
В Python эти принципы сочетаются с динамической природой языка, создавая гибкие, тестируемые системы, которые эволюционируют вместе с бизнес-требованиями без накопления технического долга.

Понимание чистой архитектуры в Python
Чистая архитектура, предложенная Робертом К. Мартином (Дядя Боб), организует программное обеспечение в концентрические слои, где зависимости направлены внутрь к ядру бизнес-логики. Этот архитектурный паттерн гарантирует, что критически важные бизнес-правила вашего приложения остаются независимыми от фреймворков, баз данных и внешних сервисов.
Основная философия
Основной принцип прост, но мощен: бизнес-логика не должна зависеть от инфраструктуры. Ваши доменные сущности, кейсы использования и бизнес-правила должны работать независимо от того, используете ли вы PostgreSQL или MongoDB, FastAPI или Flask, AWS или Azure.
В Python эта философия идеально сочетается с “утиной типизацией” и ориентированным на протоколы программированием языка, позволяя чистое разделение без формальностей, необходимых в статически типизированных языках.
Четыре слоя чистой архитектуры
Слой сущностей (Домен): Чистые бизнес-объекты с правилами, применимыми на уровне предприятия. Это POJOs (Plain Old Python Objects) без внешних зависимостей.
Слой кейсов использования (Приложение): Бизнес-правила, специфичные для приложения, которые организуют поток данных между сущностями и внешними сервисами.
Слой адаптеров интерфейсов: Преобразует данные между форматом, удобным для кейсов использования и сущностей, и форматом, требуемым внешними агентами.
Слой фреймворков и драйверов: Все внешние детали, такие как базы данных, веб-фреймворки и внешние API.
Принципы SOLID в Python
Принципы SOLID составляют основу чистой архитектуры. Давайте рассмотрим, как каждый принцип проявляется в Python. Для всестороннего обзора шаблонов проектирования в Python см. Руководство по шаблонам проектирования в Python.
Принцип единственной ответственности (SRP)
Каждый класс должен иметь одну причину для изменения:
# Плохо: Множество ответственностей
class UserManager:
def create_user(self, user_data):
# Создание пользователя
pass
def send_welcome_email(self, user):
# Отправка email
pass
def log_creation(self, user):
# Логирование в файл
pass
# Хорошо: Разделенные ответственности
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"Пользователь создан: {user.id}")
return user
Принцип открытости/закрытости (OCP)
Программные сущности должны быть открыты для расширения, но закрыты для модификации:
from abc import ABC, abstractmethod
from typing import Protocol
# Использование Protocol (Python 3.8+)
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Логика кредитной карты
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Логика PayPal
return True
# Легко расширяется без модификации существующего кода
class CryptoProcessor:
def process_payment(self, amount: float) -> bool:
# Логика криптовалюты
return True
Принцип подстановки Лисков (LSP)
Объекты должны быть заменяемыми их подтипами без нарушения работы программы:
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:
# Реализация PostgreSQL
pass
def get(self, key: str) -> str:
# Реализация PostgreSQL
return ""
class RedisStore(DataStore):
def save(self, key: str, value: str) -> None:
# Реализация Redis
pass
def get(self, key: str) -> str:
# Реализация Redis
return ""
# Оба могут использоваться взаимозаменяемо
def process_data(store: DataStore, key: str, value: str):
store.save(key, value)
return store.get(key)
Принцип разделения интерфейса (ISP)
Клиенты не должны быть вынуждены зависеть от интерфейсов, которые они не используют:
# Плохо: Толстый интерфейс
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep(self): pass
# Хорошо: Разделенные интерфейсы
class Workable(Protocol):
def work(self) -> None: ...
class Eatable(Protocol):
def eat(self) -> None: ...
class Human:
def work(self) -> None:
print("Работаю")
def eat(self) -> None:
print("Ем")
class Robot:
def work(self) -> None:
print("Работаю")
# Метод eat не нужен
Принцип инверсии зависимостей (DIP)
Высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба должны зависеть от абстракций:
from typing import Protocol
# Абстракция
class EmailSender(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
# Низкоуровневый модуль
class SMTPEmailSender:
def send(self, to: str, subject: str, body: str) -> None:
# Реализация SMTP
pass
# Высокоуровневый модуль зависит от абстракции
class UserRegistrationService:
def __init__(self, email_sender: EmailSender):
self.email_sender = email_sender
def register(self, email: str, name: str):
# Логика регистрации
self.email_sender.send(
to=email,
subject="Добро пожаловать!",
body=f"Привет {name}"
)
Паттерн Репозиторий: Абстрагирование доступа к данным
Паттерн Репозиторий предоставляет коллекционный интерфейс для доступа к доменным объектам, скрывая детали хранения данных.
Базовая реализация репозитория
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
Реализация с 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
Репозиторий в памяти для тестирования
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
Сервисный слой: Оркестрация бизнес-логики
Сервисный слой реализует сценарии использования и оркестрирует поток между репозиториями, внешними сервисами и доменной логикой.
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:
# Проверка существования пользователя
existing_user = self.user_repository.get_by_email(email)
if existing_user:
raise UserAlreadyExistsError(f"Пользователь с email {email} уже существует")
# Создание нового пользователя
user = User(
id=uuid4(),
email=email,
name=name,
is_active=True
)
# Сохранение в репозиторий
user = self.user_repository.save(user)
# Отправка приветственного письма
self.email_service.send(
to=user.email,
subject="Добро пожаловать!",
body=f"Привет {user.name}, добро пожаловать на нашу платформу!"
)
# Публикация события
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"Пользователь {user_id} не найден")
user.is_active = False
user = self.user_repository.save(user)
self.event_publisher.publish('user.deactivated', {
'user_id': str(user.id)
})
return user
Внедрение зависимостей в Python
Динамическая природа Python делает внедрение зависимостей простым без необходимости использования сложных фреймворков.
Конструкторное внедрение
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):
# Использование внедренных зависимостей
pass
Простой контейнер зависимостей
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"Регистрация не найдена для {interface}")
# Использование
def create_container() -> Container:
container = Container()
# Регистрация сервисов
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
Гексагональная архитектура (Порты и адаптеры)
Гексагональная архитектура размещает бизнес-логику в центре с адаптерами, обрабатывающими внешнюю коммуникацию.
Определение портов (интерфейсов)
# Входной порт (основной)
class CreateUserUseCase(Protocol):
def execute(self, request: 'CreateUserRequest') -> 'CreateUserResponse':
...
# Выходной порт (вторичный)
class UserPersistencePort(Protocol):
def save(self, user: User) -> User:
...
def find_by_email(self, email: str) -> Optional[User]:
...
Реализация адаптеров
from pydantic import BaseModel, EmailStr
# Входной адаптер (REST API)
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))
# Выходной адаптер (база данных)
# Уже реализован как SQLAlchemyUserRepository
Паттерны Domain-Driven Design
Value Objects
Неизменяемые объекты, определенные своими атрибутами:
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"Неверный email: {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("Сумма не может быть отрицательной")
if self.currency not in ['USD', 'EUR', 'GBP']:
raise ValueError(f"Неподдерживаемая валюта: {self.currency}")
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError("Нельзя складывать разные валюты")
return Money(self.amount + other.amount, self.currency)
Агрегаты
Кластер доменных объектов, рассматриваемых как единое целое:
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("Нельзя подтвердить пустой заказ")
if self.status != "pending":
raise ValueError("Заказ уже обработан")
self.status = "confirmed"
Доменные события
Доменные события обеспечивают слабую связанность между компонентами и поддерживают архитектуры, ориентированные на события. Для масштабируемых систем, ориентированных на события, рассмотрите реализацию потоковой передачи событий с сервисами вроде AWS Kinesis — см. Создание микросервисов, ориентированных на события, с использованием AWS Kinesis для детального руководства.
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)
Современные возможности Python для реализации чистой архитектуры
Python’s modern features make implementing clean architecture more elegant and type-safe. If you need a quick reference for Python syntax and features, check out the Python Cheatsheet.
Типы и Протоколы
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 для валидации
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('Name cannot contain numbers')
return v
class Config:
frozen = True # Make immutable
Async/Await для операций ввода-вывода
Python’s async/await syntax is particularly powerful for I/O-bound operations in clean architecture, allowing non-blocking interactions with databases and external services. When deploying Python applications to serverless platforms, understanding performance characteristics becomes crucial—see AWS lambda performance: JavaScript vs Python vs Golang for insights on optimizing Python serverless functions.
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]
Лучшие практики структуры проекта
Правильная организация проекта важна для поддержания чистой архитектуры. Перед настройкой структуры проекта убедитесь, что вы используете виртуальные окружения Python для изоляции зависимостей. venv Cheatsheet содержит всю необходимую информацию о управлении виртуальными окружениями. Для современных проектов на Python рассмотрите использование uv - New Python Package, Project, and Environment Manager, который обеспечивает более быстрый менеджмент пакетов и настройку проектов.
my_application/
├── domain/ # Предприятие бизнес-правила
│ ├── __init__.py
│ ├── entities/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── order.py
│ ├── value_objects/
│ │ ├── __init__.py
│ │ ├── email.py
│ │ └── money.py
│ ├── events/
│ │ ├── __init__.py
│ │ └── user_events.py
│ └── exceptions.py
│
├── application/ # Приложение бизнес-правила
│ ├── __init__.py
│ ├── use_cases/
│ │ ├── __init__.py
│ │ ├── create_user.py
│ │ └── place_order.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── ports/
│ ├── __init__.py
│ ├── repositories.py
│ └── external_services.py
│
├── infrastructure/ # Внешние интерфейсы
│ ├── __init__.py
│ ├── persistence/
│ │ ├── __init__.py
│ │ ├── sqlalchemy/
│ │ │ ├── models.py
│ │ │ └── repositories.py
│ │ └── mongodb/
│ │ └── repositories.py
│ ├── messaging/
│ │ ├── __init__.py
│ │ └── rabbitmq_publisher.py
│ ├── external_services/
│ │ ├── __init__.py
│ │ └── email_service.py
│ └── config.py
│
├── presentation/ # UI/API слой
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ ├── routes/
│ │ │ ├── __init__.py
│ │ │ ├── users.py
│ │ │ └── orders.py
│ │ └── schemas/
│ │ ├── __init__.py
│ │ └── user_schemas.py
│ └── cli/
│ └── commands.py
│
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── main.py # Точка входа приложения
├── container.py # Настройка инъекции зависимостей
├── pyproject.toml
└── README.md
Тестирование чистой архитектуры
Юнит-тестирование доменной логики
import pytest
from uuid import uuid4
def test_user_creation():
user = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
assert user.email == "test@example.com"
assert user.is_active is True
def test_order_total_calculation():
order = Order(id=uuid4(), customer_id=uuid4())
order.add_item(
uuid4(),
quantity=2,
price=Money(10.0, "USD")
)
order.add_item(
uuid4(),
quantity=1,
price=Money(5.0, "USD")
)
assert order.total().amount == 25.0
Интеграционное тестирование с репозиторием
@pytest.fixture
def in_memory_repository():
return InMemoryUserRepository()
def test_user_repository_save_and_retrieve(in_memory_repository):
user = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
saved_user = in_memory_repository.save(user)
retrieved_user = in_memory_repository.get_by_id(user.id)
assert retrieved_user is not None
assert retrieved_user.email == user.email
Тестирование слоя сервисов
from unittest.mock import Mock
def test_user_registration():
# Arrange
mock_repository = Mock(spec=UserRepository)
mock_repository.get_by_email.return_value = None
mock_repository.save.return_value = User(
id=uuid4(),
email="test@example.com",
name="Test"
)
mock_email = Mock(spec=EmailSender)
mock_events = Mock(spec=EventPublisher)
service = UserService(mock_repository, mock_email, mock_events)
# Act
user = service.register_user("test@example.com", "Test")
# Assert
assert user.email == "test@example.com"
mock_repository.save.assert_called_once()
mock_email.send.assert_called_once()
mock_events.publish.assert_called_once()
Общие ошибки и способы их избежания
Переинжиниринг
Не реализуйте чистую архитектуру для простых CRUD-приложений. Начните с простого и рефакторите по мере роста сложности.
Утечки абстракций
Убедитесь, что доменные сущности не содержат аннотаций базы данных или специфичного для фреймворка кода:
# Плохо
from sqlalchemy import Column
@dataclass
class User:
id: Column(Integer, primary_key=True) # Утечка фреймворка в домен
# Хорошо
@dataclass
class User:
id: UUID # Чистая доменная сущность
Циклические зависимости
Используйте инъекцию зависимостей и интерфейсы для устранения циклических зависимостей между слоями.
Игнорирование контекста
Чистая архитектура не является универсальным решением. Настройте строгость слоев в зависимости от размера проекта и опыта команды.
Полезные ссылки
- Clean Architecture by Robert C. Martin
- Python Type Hints Documentation
- Pydantic Documentation
- FastAPI Official Docs
- SQLAlchemy ORM Documentation
- Dependency Injector Library
- Domain-Driven Design Reference
- Architecture Patterns with Python
- Martin Fowler’s Blog on Architecture
- Python Design Patterns Guide
- Python Cheatsheet
- venv Cheatsheet
- uv - New Python Package, Project, and Environment Manager
- AWS lambda performance: JavaScript vs Python vs Golang
- Building Event-Driven Microservices with AWS Kinesis