← Назад к вопросам
Как достучаться к доменной логике?
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. Практическая рекомендация
Большинство приложений должны использовать:
-
REST API + shared models — стандартный подход
- Simple CRUD на frontend через REST
- Сложная доменная логика на backend
- Некоторые простые операции (валидация, расчёты) на frontend для UX
-
Структура:
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
Ключевой вывод
Фронтенд достаёт доменную логику через:
- REST API — основной способ для синхронных операций
- GraphQL — для более сложных queries
- WebSocket — для real-time обновлений
- Shared models — копирование простой логики для UX (но backend всегда авторитетен)
- Backend-driven UI — когда бизнес-логика очень сложная
Главное правило: Backend — source of truth для доменной логики. Frontend может делать локальную валидацию для UX, но финальное решение принимает backend.