Этап 10 - Mapper и стратегии преобразования
Mapper преобразует данные между моделями: entity в DTO, request DTO в command, domain object в response или payload внешнего API во внутреннюю модель. Mapping кажется скучным, пока через него не начинают вытекать lazy-loading проблемы, security fields, null semantics или случайные изменения одного слоя в другой.

Почему это важно
Architecture в backend-разработке отвечает за то, где код имеет право жить и что он имеет право знать. Без архитектуры каждая feature превращается в прямую дорожку от HTTP к database, от database к external service и обратно. Для маленького demo это может работать, но в реальном продукте такой shortcut создает хрупкий код. Изменение database протекает в API. Validation rule появляется в трех services. Transaction начинается не там. Class невозможно протестировать без запуска всего application.
Хорошая архитектура нужна не для красивых diagrams. Она нужна, чтобы уменьшить стоимость изменений. Developer должен уметь добавить field, изменить rule, заменить external provider или протестировать use case без переписывания unrelated parts системы. Design должен делать нормальный путь очевидным, а опасный путь трудным.
Как об этом думать
Начинайте с ответственности. Спросите, какое решение принадлежит конкретному коду. Controller отвечает за HTTP details. Use case отвечает за application flow. Repository отвечает за persistence access. Mapper отвечает за data conversion. Transaction boundary отвечает за atomicity. Когда эти ответственности смешаны, bugs сложнее найти, потому что один class делает несколько работ по нескольким причинам.
Потом смотрите на направление зависимостей. Dependencies обычно должны идти от внешних деталей к устойчивым внутренним правилам или от high-level policy к заменяемым details через interfaces. Если domain rule импортирует web controller, направление неправильное. Если service создает HTTP client прямо внутри method, dependency скрыта. Если mapper случайно загружает lazy relations из database, boundary протекает.
Конкретный пример
@Service
public class CreateOrderUseCase {
private final OrderRepository orders;
private final PaymentPort payments;
@Transactional
public OrderResult create(CreateOrderCommand command) {
Order order = Order.create(command.customerId(), command.items());
payments.reserve(order.total());
return OrderResult.from(orders.save(order));
}
}
Пример маленький, но в нем видны архитектурные решения. Use case получает dependencies, а не создает их сам. Payment system представлен port, а не конкретным SDK class. Transaction boundary окружает business operation. API layer может превратить request DTO в CreateOrderCommand до вызова этого кода, а response DTO можно собрать после возврата результата.
Полезная таблица
| Понятие | Значение |
|---|---|
| Controller | HTTP input и output |
| Use case | Application flow |
| Repository | Persistence access |
| Boundary | Граница ответственности |
Частые ошибки
- Считать папки архитектурой, хотя dependencies все еще указывают куда угодно.
- Добавлять abstractions до реальной причины менять implementation.
- Оставлять business rules в controllers, потому что сегодня так быстрее.
- Возвращать entities из API endpoints и называть это простотой.
- Ставить transaction boundaries вокруг helper methods вместо use cases.
Чеклист понимания
- Я могу объяснить, какой layer или boundary владеет решением в этой теме.
- Я понимаю, какие dependencies должны смотреть внутрь, а какие оставаться снаружи.
- Я могу назвать конкретный bug, который предотвращает такой design.
- Я могу понять, когда principle используют чрезмерно.
Практика перед следующим уроком
Возьмите маленькую feature создания заказа и нарисуйте, где должны жить controller, DTO, mapper, use case, repository, external payment adapter и transaction boundary. Затем отметьте одну dependency, которая была бы опасной, если направить ее в обратную сторону.