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

Как реализовать пользовательский интерфейс с вычислениями чтобы не дублировалась бизнес-логика в гексагональной архитектуре?

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

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

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

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

Проблема в архитектуре

При неправильной реализации бизнес-логика дублируется между фронтенд и бэкенд. В гексагональной архитектуре нужно четко разделить ответственность.

Решение 1: Разделение ответственности

// backend/domain/invoice.py - бизнес-логика (ОДИН раз)
class InvoiceCalculator:
    def calculate_total(self, items, tax_rate):
        subtotal = sum(item.price * item.quantity for item in items)
        tax = subtotal * tax_rate
        return subtotal + tax

// backend/application/invoice_service.py
class InvoiceService:
    def __init__(self, repository, calculator):
        self.repository = repository
        self.calculator = calculator
    
    def create_invoice(self, items, tax_rate):
        total = self.calculator.calculate_total(items, tax_rate)
        invoice = Invoice(items=items, total=total)
        return self.repository.save(invoice)

// backend/presentation/api.py
@router.post("/invoices")
def create_invoice(request):
    invoice = invoice_service.create_invoice(request.items, request.tax_rate)
    return {
        "id": invoice.id,
        "total": invoice.total,  // Уже вычислено на бэкенде
        "items": invoice.items
    }

Решение 2: Фронтенд использует API (НЕ дублирует логику)

// frontend/services/invoiceApi.ts
export const invoiceApi = {
  createInvoice: async (items, taxRate) => {
    const response = await fetch('/api/v1/invoices', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ items, tax_rate: taxRate })
    });
    return response.json();
  }
};

// frontend/hooks/useInvoice.ts
export function useInvoice() {
  const [invoice, setInvoice] = useState(null);
  const [loading, setLoading] = useState(false);

  const createInvoice = async (items, taxRate) => {
    setLoading(true);
    try {
      // Все вычисления на бэкенде!
      const data = await invoiceApi.createInvoice(items, taxRate);
      setInvoice(data);
    } finally {
      setLoading(false);
    }
  };

  return { invoice, loading, createInvoice };
}

// frontend/components/InvoiceForm.tsx
export function InvoiceForm() {
  const { invoice, loading, createInvoice } = useInvoice();

  const handleSubmit = async (items) => {
    // НЕ вычисляем total здесь
    await createInvoice(items, 0.1);
  };

  return (
    <form onSubmit={handleSubmit}>
      {invoice && <p>Total: {invoice.total}</p>}
    </form>
  );
}

Решение 3: Кэширование результатов без дублирования

// frontend/lib/cache.ts
class CachedInvoiceService {
  private cache = new Map();

  getInvoice(id) {
    return this.cache.get(id) || null;
  }

  async fetchInvoice(id) {
    const cached = this.getInvoice(id);
    if (cached) return cached;

    const invoice = await invoiceApi.getInvoice(id);
    this.cache.set(id, invoice);
    return invoice;
  }

  invalidate(id) {
    this.cache.delete(id);
  }
}

Решение 4: Отображение формул (только для предпросмотра)

// frontend/components/InvoicePreview.tsx
export function InvoicePreview({ items, taxRate }) {
  const [calculation, setCalculation] = useState(null);

  // Mock-вычисления ТОЛЬКО для предпросмотра
  const calculatePreview = (items, tax) => {
    const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    const taxAmount = subtotal * tax;
    return {
      subtotal,
      tax: taxAmount,
      total: subtotal + taxAmount
    };
  };

  useEffect(() => {
    setCalculation(calculatePreview(items, taxRate));
  }, [items, taxRate]);

  return (
    <div>
      <p>Total: {calculation?.total}</p>
      <button onClick={() => submitInvoice()}>Confirm</button>
    </div>
  );
}

// submitInvoice вызывает API - бэкенд пересчитает все
async function submitInvoice() {
  const result = await invoiceApi.createInvoice(items, taxRate);
  displayInvoice(result);
}

Решение 5: Общие типы между фронтом и бэком

// shared/types/invoice.ts
export interface Invoice {
  id: string;
  items: Item[];
  subtotal: number;
  tax: number;
  total: number;
  createdAt: Date;
}

export interface Item {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

// frontend/types/invoice.ts
import type { Invoice, Item } from 'shared/types';

const invoice: Invoice = apiResponse;

Ключевые принципы

  1. Бизнес-логика на бэкенде - вычисления только там
  2. Фронтенд как представление - отправляет данные, отображает
  3. API как граница - четкий контракт
  4. Кэширование для UX - но не дублирование логики
  5. Один источник истины - бэкенд решает, фронтенд показывает
Как реализовать пользовательский интерфейс с вычислениями чтобы не дублировалась бизнес-логика в гексагональной архитектуре? | PrepBro