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 с чёткими портами.

Связанные темы

Ресурсы