← Назад к вопросам

Как достучаться к доменной логике?

1.3 Junior🔥 101 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как достучаться к доменной логике

Этот вопрос очень важен для frontend-инженера. Доменная логика — это сердце приложения, и frontend должен правильно с ней взаимодействовать. Расскажу про разные подходы и когда их использовать.

1. REST API — классический способ

// Frontend слой
class OrderService {
  async createOrder(items) {
    const response = await fetch('/api/v1/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ items })
    });
    
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    
    return response.json();
  }

  async validateOrder(order) {
    const response = await fetch('/api/v1/orders/validate', {
      method: 'POST',
      body: JSON.stringify(order)
    });
    return response.json();
  }
}

// Backend имеет доменную логику
// POST /api/v1/orders
// - Валидирует заказ
// - Проверяет стоимость
// - Создаёт запись в БД
// - Отправляет email

Плюсы:

  • Простая архитектура
  • Frontend не зависит от backend реализации
  • Легко тестировать (мокируем HTTP)

Минусы:

  • Много network requests
  • Синхронизация может быть сложной

2. GraphQL — более гибкий способ

// Frontend запрашивает ровно то, что нужно
const VALIDATE_ORDER = gql`
  mutation ValidateOrder($items: [CartItem!]!) {
    validateOrder(items: $items) {
      valid
      errors
      totalPrice
    }
  }
`;

function OrderForm() {
  const [validateOrder] = useMutation(VALIDATE_ORDER);
  
  const handleSubmit = async (items) => {
    const result = await validateOrder({ variables: { items } });
    if (result.data.validateOrder.valid) {
      // Доменная логика ответила, что заказ валиден
      submitOrder(items);
    }
  };
  
  return <form onSubmit={handleSubmit}>...</form>;
}

Плюсы:

  • Запрашиваешь только нужные данные
  • Одна точка входа (вместо 10 REST endpoints)
  • Батчинг запросов

Минусы:

  • Сложнее настроить
  • Может быть overkill для простого приложения

3. Backend-driven UI — доменная логика управляет UI

// Backend возвращает не только данные, но и инструкции
const response = await fetch('/api/v1/product/123');
const data = await response.json();

// {
//   "id": 123,
//   "name": "Product",
//   "price": 100,
//   "actions": [
//     {
//       "type": "button",
//       "label": "Buy",
//       "handler": "purchase",
//       "enabled": true
//     },
//     {
//       "type": "button",
//       "label": "Add to wishlist",
//       "handler": "wishlist",
//       "enabled": false
//     }
//   ]
// }

function ProductCard({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      {product.actions.map(action => (
        <button
          key={action.type}
          disabled={!action.enabled}
          onClick={() => handleAction(action.handler)}
        >
          {action.label}
        </button>
      ))}
    </div>
  );
}

Плюсы:

  • Доменная логика полностью контролирует UI
  • Легко менять правила (не нужно выкатывать новый frontend)
  • Централизованное управление

Минусы:

  • Меньше контроля на frontend
  • Backend tight coupling с UI

4. Shared domain model — копирование логики

// Доменная логика частично на frontend

// Модель заказа (используется везде)
class Order {
  items: OrderItem[];
  shippingAddress: Address;
  
  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
  
  applyDiscount(discount: Discount): void {
    if (this.isEligible(discount)) {
      this.discount = discount;
    }
  }
  
  private isEligible(discount: Discount): boolean {
    return this.calculateTotal() >= discount.minAmount;
  }
  
  validate(): ValidationResult {
    if (this.items.length === 0) return { valid: false, error: 'Empty order' };
    if (!this.shippingAddress) return { valid: false, error: 'No address' };
    return { valid: true };
  }
}

// Frontend использует модель
function OrderForm() {
  const [order, setOrder] = useState(new Order());
  
  const handleAddItem = (item: OrderItem) => {
    order.items.push(item);
    setOrder({ ...order }); // Обновляем state
  };
  
  const handleApplyDiscount = (discount: Discount) => {
    order.applyDiscount(discount);
    setOrder({ ...order });
  };
  
  const handleSubmit = async () => {
    const validation = order.validate();
    if (!validation.valid) {
      alert(validation.error);
      return;
    }
    
    // Отправляем на backend для финальной валидации и сохранения
    const response = await fetch('/api/v1/orders', {
      method: 'POST',
      body: JSON.stringify(order)
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>Total: {order.calculateTotal()}</div>
      ...
    </form>
  );
}

// Backend имеет ту же модель (или похожую на другом языке)
// class Order {
//   def calculate_total(self):
//     return sum(item.price * item.quantity for item in self.items)
//   def validate(self):
//     ...
// }

Плюсы:

  • Frontend может работать offline
  • Быстрая валидация без network
  • User experience лучше (instant feedback)

Минусы:

  • Дублирование кода
  • Синхронизация моделей сложная
  • Risk: frontend может отличаться от backend логики

5. Event-driven архитектура — через события

// Frontend отправляет события, backend их обрабатывает
class OrderEventEmitter extends EventTarget {
  createOrder(items) {
    // Валидируем локально
    if (items.length === 0) {
      this.dispatchEvent(new CustomEvent('orderError', {
        detail: { message: 'Empty order' }
      }));
      return;
    }
    
    // Отправляем событие на backend
    fetch('/api/v1/events', {
      method: 'POST',
      body: JSON.stringify({
        type: 'OrderCreated',
        data: { items, timestamp: new Date() }
      })
    });
    
    this.dispatchEvent(new CustomEvent('orderCreated', {
      detail: { items }
    }));
  }
}

const orderEmitter = new OrderEventEmitter();

orderEmitter.addEventListener('orderCreated', (e) => {
  console.log('Order created:', e.detail);
  // Обновляем UI
});

orderEmitter.addEventListener('orderError', (e) => {
  console.error('Error:', e.detail.message);
});

orderEmitter.createOrder(items);

Плюсы:

  • Слабая связанность (coupling)
  • Легко добавить новых слушателей
  • Асинхронность встроена

Минусы:

  • Сложнее отладка
  • Порядок событий может быть непредсказуем

6. WebSocket для real-time доменной логики

class RealtimeOrderService {
  constructor() {
    this.ws = new WebSocket('wss://api.example.com/orders');
    this.ws.onmessage = (e) => {
      const update = JSON.parse(e.data);
      this.handleDomainUpdate(update);
    };
  }
  
  createOrder(items) {
    this.ws.send(JSON.stringify({
      action: 'createOrder',
      payload: { items }
    }));
  }
  
  handleDomainUpdate(update) {
    // Backend отправляет обновления доменной логики
    // { type: 'OrderValidated', order: {...} }
    // { type: 'PaymentProcessed', orderId: 123 }
    // { type: 'OrderConfirmed', orderId: 123 }
    
    switch (update.type) {
      case 'OrderValidated':
        this.onOrderValidated(update.order);
        break;
      case 'PaymentProcessed':
        this.onPaymentProcessed(update.orderId);
        break;
      // ...
    }
  }
  
  onOrderValidated(order) {
    // Обновляем UI
    console.log('Order is valid:', order);
  }
}

Когда использовать: Для real-time приложений (чаты, коллаборация, live updates).

7. Практическая рекомендация

Большинство приложений должны использовать:

  1. REST API + shared models — стандартный подход

    • Simple CRUD на frontend через REST
    • Сложная доменная логика на backend
    • Некоторые простые операции (валидация, расчёты) на frontend для UX
  2. Структура:

    frontend/
    ├── domain/          // Общие модели, которые есть на backend
    │   ├── Order.ts
    │   ├── Cart.ts
    │   └── Discount.ts
    ├── services/        // Слой общения с backend
    │   ├── OrderService.ts
    │   └── PaymentService.ts
    ├── hooks/           // React hooks
    │   ├── useOrder.ts
    │   └── useCart.ts
    └── components/      // UI компоненты
    
    backend/
    ├── domain/          // Подробная доменная логика
    │   ├── Order
    │   │   ├── Order.ts
    │   │   ├── OrderValidator.ts
    │   │   └── OrderFactory.ts
    │   └── ...
    ├── application/     // Use cases, сервисы
    ├── infrastructure/  // БД, внешние API
    └── api/             // REST endpoints
    

Ключевой вывод

Фронтенд достаёт доменную логику через:

  1. REST API — основной способ для синхронных операций
  2. GraphQL — для более сложных queries
  3. WebSocket — для real-time обновлений
  4. Shared models — копирование простой логики для UX (но backend всегда авторитетен)
  5. Backend-driven UI — когда бизнес-логика очень сложная

Главное правило: Backend — source of truth для доменной логики. Frontend может делать локальную валидацию для UX, но финальное решение принимает backend.