Clean Architecture на фронтенде
Clean Architecture на фронтенде — подход к структуре кода, при котором бизнес-логика изолирована от фреймворка (React/Vue) и инфраструктуры (API, localStorage), что делает её независимо тестируемой и переиспользуемой.
Зачем нужно
В типичном React-приложении бизнес-логика перемешана с компонентами: useEffect делает запрос, обрабатывает ответ, вычисляет итоговую цену. Тестировать такое сложно — нужен полный рендер компонента с моками. Clean Architecture извлекает логику в чистые функции и классы, не зависящие от React. Тест бизнес-правила — это просто expect(calculateDiscount(order)).toBe(50).
Где используется
- Крупные enterprise-приложения с многолетней поддержкой
- Проекты с планируемой миграцией фреймворка (Angular → React)
- Команды с опытом Domain-Driven Design
- Совместно с Feature-Sliced Design на уровне ядра домена
Слои Clean Architecture
┌─────────────────────────────────┐
│ UI Layer (React) │ ← Компоненты, хуки
│ Зависит от всего ниже │
├─────────────────────────────────┤
│ Application Layer │ ← Use Cases (сценарии)
│ Оркестрирует логику │
├─────────────────────────────────┤
│ Domain Layer │ ← Сущности, бизнес-правила
│ Не знает о React и API │
├─────────────────────────────────┤
│ Infrastructure Layer │ ← API, localStorage, WebSocket
│ Реализует интерфейсы │
└─────────────────────────────────┘
Правило зависимостей: стрелки идут только ВНУТРЬ
Domain не знает о React, инфраструктуре и даже Use Cases
Пример: корзина покупок
// domain/Cart.ts — чистая бизнес-логика, нет React, нет fetch
export interface CartItem {
productId: string;
name: string;
price: number;
quantity: number;
}
export class Cart {
private items: CartItem = ;
addItem(item: CartItem): void {
const existing = this.items.find(i => i.productId === item.productId);
if (existing) {
existing.quantity += item.quantity;
} else {
this.items.push({ ...item });
}
}
removeItem(productId: string): void {
this.items = this.items.filter(i => i.productId !== productId);
}
// Бизнес-правило: скидка 10% при сумме > 5000
getTotal: number {
const subtotal = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
return subtotal > 5000 ? subtotal * 0.9 : subtotal;
}
getItems: CartItem {
return [...this.items]; // иммутабельный доступ
}
}
// infrastructure/CartRepository.ts — работает с localStorage
export interface ICartRepository {
save(items: CartItem): void;
load: CartItem;
}
export class LocalStorageCartRepository implements ICartRepository {
private KEY = 'cart';
save(items: CartItem): void {
localStorage.setItem(this.KEY, JSON.stringify(items));
}
load: CartItem {
try {
return JSON.parse(localStorage.getItem(this.KEY) || '');
} catch {
return ;
}
}
}
// application/AddToCartUseCase.ts — оркестрирует
export class AddToCartUseCase {
constructor(
private cart: Cart,
private repository: ICartRepository,
) {}
execute(item: CartItem): void {
this.cart.addItem(item);
this.repository.save(this.cart.getItems); // сохраняем после изменения
}
}
// ui/CartButton.jsx — только UI, логика делегирована
function AddToCartButton({ product }) {
const { addToCart } = useCart; // хук скрывает Use Case
return (
<button onClick={ => addToCart(product)}>
В корзину
</button>
);
}
Частые ошибки
- Ovengineering для простых проектов — Clean Architecture оправдан при сложном домене и большой команде; стартап из 2 человек просто замедлится.
- Circular dependencies — Domain импортирует из Infrastructure, нарушая правило зависимостей; используйте интерфейсы и Dependency Injection.
- Слой Application = просто прокси — Use Cases должны содержать реальную оркестрацию, а не тривиально вызывать один метод репозитория.
🎓 Источники
- 🎓 [Почему на фронтенде не выходит DDD и Clean architecture] и Илья Климов · 2025-02-23 · YouTube
- Альтернативная позиция: на фронте Clean Architecture/DDD плохо приживаются — фронт мечется между фреймворками каждые 2-3 года. То, что в одном фреймворке элегантно, в следующем уже не вписывается.
- DDD не сводится к доменной логике: это ещё агрегаты, проекции, разные интерфейсы одной сущности в разных бизнес-процессах.
- 🎓 [Metarhia Weekly #192 — стыковка DDD фронт+бэк] · 2025-03-01 · YouTube
- Local-first как направление, где Clean Architecture на фронте действительно работает: домен + реактивная локальная СУБД.
- 🎓 [Public Interview #7 — Hexagonal] · 2022-05-13 · YouTube
- Гексагональная — реинкарнация Clean Architecture с чёткими портами.
Связанные темы
- _MOC SPA
- Feature-Sliced Design
- Dependency Injection для тестируемости
- MVVM -- Model View ViewModel
- DDD — обзор
- Hexagonal Architecture