А почему мы не пишем код в контролерах?
Я думаю многие из Вас слышали мнение о том что кода в контроллерах быть не должно, и потому контроллер с методами в одну строку считаются «Best Practice».Я в свою очередь сомневаюсь в том, что польза от этого так уж велика. Если у Вас возникали похожие мысли, прошу под кат.
Всем привет! Сразу хочу сказать что моё мнение не является истинной, и цель сего поста это высказать своё мнение, и услышать комментарии других. Все сказанное относится в реализации API, если у вас MVC с вьюшками, то этот кейс не сработает ибо в таком случае в контроллерах лучше писать уже только логику с View
Что не так?
Я точно уверен что многие из вас работают с типичным CRUD приложение с 3-х слойной архитектурой (больше слоев кстати нет, но об этом как то в следующий раз). И в этой архитектуре у вас есть слой работы с данными (дальше DA), слой бизнес логики (дальше BL), и слой вью (дальше VL).
BL может быть сделан по разному, я встречал 2 варианта
- Class — просто класс в котором есть куча зависимостей и методы. Каждый метод описывает какой-то бизнес флоу, к примеру передача денег, авторизация, регистрация. Этот класс использует более низкоуровневые вещи такие как IRepository для работы с бд, различные API клиенты для других сервисов и тому подобное, в общем на этом слое собирают все модули вместе и делают бизнес логику.
- CQS — На каждый бизнес флоу создают DTO (Command\Query) классы, это просто входящие параметры в наш обработчик. Этот способ становится более популярен так как лучше делится ответственность и этот обработчик не имеет так много зависимостей.
У этих способов есть ряд своих правил и одно из них это то что обработчики не могут вызывать другие обработчики, они должны быть полностью самостоятельны, точно такая же ситутация в реализации через обычный класс с кучей методов, методы на столько привязаны к фиче что не могут быть переиспользованы.
Рассмотрим пару реализаций в этом стиле, а потом я перенесу код в контроллеры и мы проанализируем что ж мы потеряли.
В таком виде мы реализовываем все методы и дальше просто их вызываем на уровне API
Я надеюсь вы понимаете что код с Command → Handler будет аналогичен, просто больше разделен.
И мне вечно не дает покоя, зачем я делаю эту дополнительную работу?
- Чтобы переиспользовать код? — Нет, флоу перевода денег не подойдет для флоу начисления бонусов.
К тому-же если вы даже найдете возможность совместить 2 фичи, вы рискуете сломав одну — автоматически сломать другую - Чтобы перенести вызов кода в другое место? — Возможно, но это бывает так часто? Да и переносить то не обязательно, кто Вам запретил резолвить экземпляр контролера закрытым под IUserService?
- Тестирование? Контроллеры точно так же тестируются, а подняв TestServer вы практически напишите end2end тесты.
А теперь посмотрим на черную магию
Давайте уберем лишнего.
О чудо, код сервиса не отличается от контролера, мы всего лишь добавили пару атрибутов и готовы ловить http запросы
Я считаю что ASP NET отлично абстрагировал нас от работы с HTTP, у нас есть наилучшее место где мы оперируем нашими типами. Повторюсь, если у Вас есть вьюшки, тогда в контроллерах лучше писать код только для View, а в сервисах писать переиспользуемые методы для получения данных для View. Но в текущих реалиях все чаще у нас API + SPA
Валидация?
ASP NET Core Pipeline очень хорошо тюнится и имеет массу решений, взгляните на FluentValidation, вы добавите валилдацию даже не меняя кода в контроллерах.
Хотите больше разделения?
Разделяйте интерфейс и если нужно реализацию тоже.
Как бонус, интерфейс сервиса становится контрактом верхнего уровня, и в рамках одного процесса это просто прямой вызов кода из контроллера, в рамках общения клиент-сервер подставляется простая реализация того-же интерфейса с использованием HttpClient.
Подключение других каналов
Если мне скажут что у нас может появиться ещё один канал, к примеру через очередь, я просто могу получить экземпляр контроллера и использовать его в другом канале. Этот контроллер легко резолвится из DI. Кроме того ASP NET достаточно гибкий и некоторые каналы можно научить его обрабатывать самому, опять-таки модифицируя пайплайн.
Забавный факт
Тестовое задание в таком стиле много где выкинут и Вам откажут, а ведь у Вас много доводов почему вы так сделали, но если вы попадете на собеседование и обсудите почему так дизайните код, то скорее всего это хорошее место и перед вами такой же специалист который понимает, что программирование многогранное
Итого
Переиспользовать можно только сервисы по типу репозитория, кеша, апи клиента и тд, но переиспользовать логику обработки одного запроса — это оооочень редкий кейс, и скорее всего плохой.
Этот подход как и другие нужно применять в нужных местах, он экономит время, и очень удобен для закрытых от публичного доступа API (микросервисов).
Я считаю что контроллеры это и есть те самые BL Service или Command\QueryHandler, и в своих проектах я практикую этот подход и контролеры делю очень хорошо, рекомендую и Вам попробовать.
Ответственность фреймворка мы ни как не увеличили, у него была задача
- Принимать запросы
- Маппить их в модели
- Вызывать указанный нами код по каким то правилам
- Отдавать ответы
Она у него и осталась
Я действительно не вижу критических проблем почему так нельзя делать, потому приглашаю вас в комментарии.
Если меня не сильно закидают то след статья будет об интерфейсах на каждом классе-сервисе системы, и нужно ли это? Стоит ли писать решение ради того чтобы было удобнее пукнуть мокнуть.