Чистая архитектура и слои приложения
Чистая архитектура Роберта Мартина, гексагональная архитектура Алистера Кокбёрна и луковая архитектура Джеффри Палермо — три названия одной идеи: бизнес-логика в центре, фреймворк и база данных — на периферии.
Почему это важно: В типичном Rails/Django-проекте бизнес-логика размазана по контроллерам, моделям и вьюхам. При смене БД, фреймворка или добавлении нового UI (CLI, API, WebSocket) приходится переписывать всё. Чистая архитектура решает эту проблему разделением на слои.
Главная идея
Правило зависимостей: зависимости направлены внутрь. Внутренний слой (бизнес-логика) ничего не знает о внешнем (фреймворк, БД, HTTP). Внешний слой адаптирует запросы внешнего мира к интерфейсам внутреннего.
Как это выглядит на практике
- Задача: перевести часть API с REST на GraphQL, сохранив бизнес-логику
- Без слоёв: бизнес-логика в контроллерах REST — нужно дублировать для GraphQL
- С чистой архитектурой: бизнес-логика в Use Cases (сервисных объектах)
- REST-контроллер вызывает CreateOrder use case с параметрами
- GraphQL-резолвер вызывает тот же CreateOrder use case
- Бизнес-логика написана один раз, два способа доставки
Примеры кода
Слои чистой архитектуры на примере заказа
# === DOMAIN LAYER (центр) — чистая бизнес-логика ===
# Не зависит от Rails, ActiveRecord, HTTP
class Order
attr_reader :items, :customer_id, :status, :total
def initialize(customer_id:, items:)
@customer_id = customer_id
@items = items
@status = :pending
@total = items.sum { |i| i[:price] * i[:quantity] }
end
def confirm!
raise 'Пустой заказ' if @items.empty?
raise 'Сумма должна быть > 0' if @total <= 0
@status = :confirmed
end
end
# === APPLICATION LAYER (use cases) ===
# Оркестрация бизнес-логики, зависит от интерфейсов (портов)
class PlaceOrder
def initialize(order_repo:, payment_gateway:, notifier:)
@order_repo = order_repo
@payment = payment_gateway
@notifier = notifier
end
def call(customer_id:, items:)
order = Order.new(customer_id: customer_id, items: items)
order.confirm!
@payment.charge(order.total, customer_id: customer_id)
@order_repo.save(order)
@notifier.order_placed(order)
order
end
end
# === INFRASTRUCTURE LAYER (адаптеры) ===
# Реализация интерфейсов: БД, API, email
class ActiveRecordOrderRepo
def save(order)
OrderRecord.create!(
customer_id: order.customer_id,
items: order.items.to_json,
total: order.total,
status: order.status
)
end
end
# === DELIVERY LAYER (контроллеры, GraphQL) ===
# REST
class Api::OrdersController < ApplicationController
def create
result = PlaceOrder.new(
order_repo: ActiveRecordOrderRepo.new,
payment_gateway: StripePayment.new,
notifier: EmailNotifier.new
).call(
customer_id: current_user.id,
items: order_params[:items]
)
render json: result, status: :created
end
end
# GraphQL — тот же use case, другая точка входа
class Mutations::PlaceOrder < BaseMutation
def resolve(items:)
PlaceOrder.new(
order_repo: ActiveRecordOrderRepo.new,
payment_gateway: StripePayment.new,
notifier: EmailNotifier.new
).call(customer_id: context[:current_user].id, items: items)
end
end
Тот же принцип на Python (FastAPI)
# domain/order.py — чистый Python, без фреймворков
from dataclasses import dataclass, field
@dataclass
class OrderItem:
product_id: int
price: float
quantity: int
@dataclass
class Order:
customer_id: int
items: list[OrderItem]
status: str = 'pending'
@property
def total(self) -> float:
return sum(i.price * i.quantity for i in self.items)
def confirm(self):
if not self.items:
raise ValueError('Order must have items')
self.status = 'confirmed'
# application/place_order.py — use case
from domain.order import Order, OrderItem
from ports.order_repo import OrderRepository
from ports.payment import PaymentGateway
class PlaceOrder:
def __init__(self, repo: OrderRepository, payment: PaymentGateway):
self.repo = repo
self.payment = payment
def execute(self, customer_id: int, items: list[dict]) -> Order:
order_items = [OrderItem(**i) for i in items]
order = Order(customer_id=customer_id, items=order_items)
order.confirm()
self.payment.charge(order.total, customer_id)
self.repo.save(order)
return order
# adapters/api.py — FastAPI controller
@router.post('/orders')
def create_order(body: OrderRequest, user=Depends(get_user)):
use_case = PlaceOrder(
repo=SqlAlchemyOrderRepo(db),
payment=StripeGateway()
)
order = use_case.execute(user.id, body.items)
return {'id': order.id, 'total': order.total}
Что происходит под капотом
- Правило зависимостей: Domain ← Application ← Infrastructure ← Delivery. Стрелка = «знает о»
- Domain layer: чистые классы без импортов фреймворка, тестируются без БД за миллисекунды
- Порты (interfaces) определяются во внутреннем слое, адаптеры (реализации) — во внешнем
- Use Case = один бизнес-сценарий, один публичный метод (call/execute), явные зависимости через конструктор
- Чистая архитектура ≠ много папок. Суть — в направлении зависимостей, не в структуре файлов
Типичные ошибки и заблуждения
- «Чистая архитектура = 20 папок и 50 файлов для одного CRUD» — для простого CRUD это overkill; применяйте, когда бизнес-логика нетривиальна
- «Нужно полностью абстрагировать ActiveRecord» — в Rails можно применять принципы выборочно: use cases без полной абстракции БД
- «Чистая, гексагональная и луковая — разные архитектуры» — одна идея с разными диаграммами: бизнес-логика в центре, фреймворк снаружи
Ключевые выводы
- Зависимости направлены внутрь: фреймворк знает о бизнес-логике, но не наоборот
- Use Case (сервисный объект) = один бизнес-сценарий с явными зависимостями
- Domain layer тестируется без БД, без HTTP, без фреймворка — за миллисекунды
Термины урока
Связь с работой backend-разработчика
Чистая архитектура — не про папки, а про направление зависимостей. В реальности: вынесите бизнес-логику в Service Objects/Use Cases, внедряйте зависимости через конструктор, и ваш код станет тестируемым и переносимым между фреймворками.
Мини-разбор реальной ситуации
SaaS-платформа для бухгалтерии: изначально монолит на Rails с бизнес-логикой в моделях и контроллерах. При добавлении мобильного API пришлось дублировать логику. После вынесения 40 ключевых операций в Use Cases: REST и GraphQL используют одну кодовую базу, тесты бизнес-логики запускаются за 3 секунды (без БД), а добавление нового endpoint — вопрос написания тонкого контроллера.
Что запомнить
- Зависимости внутрь: Domain ← Application ← Infrastructure ← Delivery
- Use Case = один сценарий, явные зависимости через конструктор, один публичный метод
- Не абстрагируйте всё сразу — начните с выноса самой сложной бизнес-логики
Итог
Чистая архитектура — это принцип, а не шаблон проекта. Бизнес-логика в центре, фреймворк и БД на периферии. Применяйте постепенно: сначала use cases для самых сложных операций, затем расширяйте по мере роста системы.
Комментарии к уроку
Войдите, чтобы оставить комментарий.
Пока нет комментариев — будьте первым.