А почему мы не пишем код в контролерах?

Я думаю многие из Вас слышали мнение о том что кода в контроллерах быть не должно, и потому контроллер с методами в одну строку считаются «Best Practice».Я в свою очередь сомневаюсь в том, что польза от этого так уж велика. Если у Вас возникали похожие мысли, прошу под кат.
image
Всем привет! Сразу хочу сказать что моё мнение не является истинной, и цель сего поста это высказать своё мнение, и услышать комментарии других. Все сказанное относится в реализации API, если у вас MVC с вьюшками, то этот кейс не сработает ибо в таком случае в контроллерах лучше писать уже только логику с View

Что не так?


Я точно уверен что многие из вас работают с типичным CRUD приложение с 3-х слойной архитектурой (больше слоев кстати нет, но об этом как то в следующий раз). И в этой архитектуре у вас есть слой работы с данными (дальше DA), слой бизнес логики (дальше BL), и слой вью (дальше VL).
BL может быть сделан по разному, я встречал 2 варианта

  • Class — просто класс в котором есть куча зависимостей и методы. Каждый метод описывает какой-то бизнес флоу, к примеру передача денег, авторизация, регистрация. Этот класс использует более низкоуровневые вещи такие как IRepository для работы с бд, различные API клиенты для других сервисов и тому подобное, в общем на этом слое собирают все модули вместе и делают бизнес логику.
  • CQS — На каждый бизнес флоу создают DTO (Command\Query) классы, это просто входящие параметры в наш обработчик. Этот способ становится более популярен так как лучше делится ответственность и этот обработчик не имеет так много зависимостей.

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

Рассмотрим пару реализаций в этом стиле, а потом я перенесу код в контроллеры и мы проанализируем что ж мы потеряли.

image
image

В таком виде мы реализовываем все методы и дальше просто их вызываем на уровне API
image

Я надеюсь вы понимаете что код с Command → Handler будет аналогичен, просто больше разделен.

И мне вечно не дает покоя, зачем я делаю эту дополнительную работу?

  • Чтобы переиспользовать код? — Нет, флоу перевода денег не подойдет для флоу начисления бонусов.
    К тому-же если вы даже найдете возможность совместить 2 фичи, вы рискуете сломав одну — автоматически сломать другую
  • Чтобы перенести вызов кода в другое место? — Возможно, но это бывает так часто? Да и переносить то не обязательно, кто Вам запретил резолвить экземпляр контролера закрытым под IUserService?
  • Тестирование? Контроллеры точно так же тестируются, а подняв TestServer вы практически напишите end2end тесты.

А теперь посмотрим на черную магию


Давайте уберем лишнего.
image
image
О чудо, код сервиса не отличается от контролера, мы всего лишь добавили пару атрибутов и готовы ловить http запросы

Я считаю что ASP NET отлично абстрагировал нас от работы с HTTP, у нас есть наилучшее место где мы оперируем нашими типами. Повторюсь, если у Вас есть вьюшки, тогда в контроллерах лучше писать код только для View, а в сервисах писать переиспользуемые методы для получения данных для View. Но в текущих реалиях все чаще у нас API + SPA

Валидация?


ASP NET Core Pipeline очень хорошо тюнится и имеет массу решений, взгляните на FluentValidation, вы добавите валилдацию даже не меняя кода в контроллерах.

Хотите больше разделения?


Разделяйте интерфейс и если нужно реализацию тоже.

image

Как бонус, интерфейс сервиса становится контрактом верхнего уровня, и в рамках одного процесса это просто прямой вызов кода из контроллера, в рамках общения клиент-сервер подставляется простая реализация того-же интерфейса с использованием HttpClient.

Подключение других каналов


Если мне скажут что у нас может появиться ещё один канал, к примеру через очередь, я просто могу получить экземпляр контроллера и использовать его в другом канале. Этот контроллер легко резолвится из DI. Кроме того ASP NET достаточно гибкий и некоторые каналы можно научить его обрабатывать самому, опять-таки модифицируя пайплайн.
image

Забавный факт


Тестовое задание в таком стиле много где выкинут и Вам откажут, а ведь у Вас много доводов почему вы так сделали, но если вы попадете на собеседование и обсудите почему так дизайните код, то скорее всего это хорошее место и перед вами такой же специалист который понимает, что программирование многогранное

Итого


Переиспользовать можно только сервисы по типу репозитория, кеша, апи клиента и тд, но переиспользовать логику обработки одного запроса — это оооочень редкий кейс, и скорее всего плохой.

Этот подход как и другие нужно применять в нужных местах, он экономит время, и очень удобен для закрытых от публичного доступа API (микросервисов).

Я считаю что контроллеры это и есть те самые BL Service или Command\QueryHandler, и в своих проектах я практикую этот подход и контролеры делю очень хорошо, рекомендую и Вам попробовать.
Ответственность фреймворка мы ни как не увеличили, у него была задача

  1. Принимать запросы
  2. Маппить их в модели
  3. Вызывать указанный нами код по каким то правилам
  4. Отдавать ответы


Она у него и осталась

Я действительно не вижу критических проблем почему так нельзя делать, потому приглашаю вас в комментарии.
Если меня не сильно закидают то след статья будет об интерфейсах на каждом классе-сервисе системы, и нужно ли это? Стоит ли писать решение ради того чтобы было удобнее пукнуть мокнуть.

© Habrahabr.ru