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

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

1. Использование слоистой архитектуры

Применяйте паттерн "Многослойная архитектура" (Layered Architecture), где бизнес-логика находится в ядре, а фреймворки и протоколы — во внешних слоях:

+-------------------+
|   Presentation    | <-- Express, Fastify, gRPC, GraphQL
+-------------------+
|     Application   | <-- Маршрутизация, валидация
+-------------------+
|      Domain       | <-- Чистая бизнес-логика
+-------------------+
| Infrastructure    | <-- БД, кэш, внешние сервисы
+-------------------+

2. Создание абстракций для взаимодействия

Определите интерфейсы (абстрактные классы или TypeScript interfaces) для всех внешних взаимодействий:

interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

interface PaymentService {
  processPayment(amount: number): Promise<PaymentResult>;
}

3. Использование DTO

Преобразуйте входящие/исходящие данные в нейтральные структуры:

class CreateOrderDto {
  constructor(
    public readonly productId: string,
    public readonly quantity: number,
    public readonly userId: string
  ) {}
}

4. Применение Dependency Injection

Внедряйте зависимости через конструкторы, а не импортируйте фреймворки напрямую:

class OrderService {
  constructor(
    private orderRepository: OrderRepository,
    private paymentService: PaymentService
  ) {}

  async createOrder(dto: CreateOrderDto) {
    // Чистая бизнес-логика
  }
}

5. Адаптеры для протоколов и фреймворков

Создавайте адаптеры, которые преобразуют специфичные запросы в нейтральные DTO:

// Express адаптер
app.post('/orders', async (req, res) => {
  const dto = new CreateOrderDto(req.body.productId, req.body.quantity, req.user.id);
  const result = await orderService.createOrder(dto);
  res.json(result);
});

// gRPC адаптер
service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse) {
    const dto = new CreateOrderDto(request.getProductId(), request.getQuantity(), request.getUserId());
    const result = await orderService.createOrder(dto);
    return new CreateOrderResponse(result);
  }
}

6. Использование Ports & Adapters

Реализуйте архитектуру, где:

  • Порты — интерфейсы для взаимодействия с внешним миром
  • Адаптеры — реализации этих интерфейсов для конкретных технологий

7. Избегание фреймворк-специфичных конструкций

Не используйте в бизнес-логике:

  • Объекты request/response фреймворков
  • Декораторы фреймворков (@Get, @Post)
  • Специфичные middleware

8. Тестируемость

Бизнес-логика должна тестироваться без моков фреймворков:

test('Order creation', async () => {
  const mockRepo = { save: jest.fn() };
  const service = new OrderService(mockRepo, mockPayment);

  await service.createOrder(new CreateOrderDto('prod1', 2, 'user1'));

  expect(mockRepo.save).toHaveBeenCalled();
});

Реальные примеры из практики

  1. NestJS: Использование сервисов в providers без декораторов
  2. CQRS: Разделение команд и запросов
  3. Domain Events: Обработка событий через абстракции

Резюмируем

  1. Бизнес-логика должна быть в отдельном слое без зависимостей от фреймворков
  2. Взаимодействие через интерфейсы, а не конкретные реализации
  3. Использовать DTO для передачи данных между слоями
  4. Адаптеры преобразуют протокол-специфичные запросы в нейтральные
  5. Тестируемость без фреймворков — главный критерий успеха

Правильно организованная архитектура позволяет:

  • Легко менять фреймворки (Express → Fastify)
  • Поддерживать разные протоколы (HTTP + gRPC)
  • Тестировать логику изолированно
  • Масштабировать систему