Всем привет! Сразу хочу сказать что моё мнение не является истинной, и цель сего поста это высказать своё мнение, и услышать комментарии других. Все сказанное относится в реализации 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, и в своих проектах я практикую этот подход и контролеры делю очень хорошо, рекомендую и Вам попробовать.
Ответственность фреймворка мы ни как не увеличили, у него была задача:
- Принимать запросы
- Маппить их в модели
- Вызывать указанный нами код по каким то правилам
- Отдавать ответы
Она у него и осталась.
Я действительно не вижу критических проблем почему так нельзя делать, потому приглашаю вас в комментарии.
Если меня не сильно закидают то след статья будет об интерфейсах на каждом классе-сервисе системы, и нужно ли это? Стоит ли писать решение ради того чтобы было удобнее
Sm1le291
«Я думаю многие из Вас слышали мнение о том что кода в контроллерах быть не должно, и потому контроллер с методами в одну строку считаются «Best Practice».Я в свою очередь сомневаюсь в том, что польза от этого так уж велика.»
Зря сомневаетесь, была бы возможность я бы вам показал код пары контроллеров крупных сайтов рунета, вы бы подофигели и не задавли бы глупых вопросов почему в контроллере кода должно быть меньше.
По той простой причине, что если его оттуда не разносить по другим местам (маррерам, сервисам, билдерам) вы очень скоро перестанете понимать что там происходит
BashkaMen Автор
Я не писал в статье о том, что мы должны отказаться от сервисов, мапперов и прочего.
Я видел контроллеры с большим и кол-вом кода, разделив его на несколько контроллеров, проблема решается.
Спрятав говно в другое место, и обернув его для вызова в одну строку, ситуация ни как не изменится.
И статья описывает ситуации где силы вкладывают в то что б в контроллере было не больше чем 1 строка в каждом методе — тоесть делать и контроллера тупой враппер.
Sm1le291
А как вы создав контроллер узнаете сколько в нем будет строк?
Если там есть какой то сервис, то должен быть наверное и маппер, уже одной строки не будет. А со временем получается что в контроллер инжектится уже штук 5-10 этих самых сервисов. Если не разносить это все, это будет ад.
Например у вас есть интерет магазин, вам нужно обработать заказ, возьмем не микросервисы, а старый класс. MVC. Вам нужно взять юзера, создать заказ, добавить в него товары, отправить это все обратно, отправить нотификейшн по e-mail и sms. Вы предлагаете все эти действия реализовать в контроллере или искуственно тут что-то поделить на несколько контроллеров?
wolfer
может будет много строк, а может и не будет. Это гадание на кофейной гуще. Если обработчик укладывается в простейшую декларативную логику, то вынос его в сервисы просто потому что так надо это лишняя работа. Я думаю автор об этом. Нет никакой проблемы, чтобы простые действия сначала писать в контроллерах, а потом вернувшись к ним за масштабными усовершенствованиями вынести их в сервис. Все должно происходить по требованию, а не ради фапа на идеальную архитектуру.
S-e-n
Напомнило письмо Кармака на эту тему.
number-none.com/blow/john_carmack_on_inlined_code.html
Simonis
Аналогично, могу сказать что такой подход совершенно не решает проблемы. Не то что видел такое, я с таким работаю в данный конкретный момент. Один метод может занимать по четыре сотни строк кода и это при учете что внутри вызывается пара десятков других сервисов. Что уже используется валидация через FluentValidator. Что логирование и обработка ошибок централировано вынесена в filters. Что используется мапер.
Мы как инженеры работаем со сложностью от которой невозможно избавиться. Ей можно пытаться манипулировать. Можно написать один метод на тысячу строк, а можно структурировать код на уровни. Вынести логику работы с базой, выделить бизнес логику, подготовку данных для отображение и закрыть все это абстракциями. Поддерживать общность и консистенцию кода, слабую связность между реализациями различных уровней. Что бы каждый кусок был прост в понимании и редактировании, но разобрав как работает один контроллер было легко понять как работают остальные.
Первый способ работы с которым учат работать в любом вузе — декомпозиция. Потому что работать с большим количеством простых вещей гораздо проще чем с чем то одним но переусложненным.
Действительно. Если проект будет жить месяц, если его не планируется поддерживать и развивать то конечно проще и целесообразнее описать весь код в контроллере и не париться. Но если планируется расширение то очень скоро вы придете к тому что разница между реализациями отдельных контроллеров начнет душить вашу архитекруту. Потому что не будет единства, не будет возможности легко вносить общность в код. Понимать каждый контролер придется с нуля потому что каждый член команды пишет по своему и смотря с какой ноги встал сегодня утром. И поверьте мне, люди чаще всего встают не с той ноги.
Личный опыт в подобных вопросах это все равно что «Синдром выжившего». И до тех пор пока вы не сталкнетесь с бизнес логикой уровня чуть выше чем «за что купил — за то продал» вряд ли удастся осознать зачем напридумали всяких там шаблонов, SOLID и написали несколько толстых книжек такие товариши как Макконел, Фаулер, Мартин.
Еще раз подчеркну: Много простого — хорошо! Сложно, но Мало — очень очень плохо! В первом случае каждый конкретный момент вы будете работать с емкой, конкретной функциональностью отдельного уровня абстрации. Во втором случае в любой момент вы будете иметь честь по локоть возиться в том самом, что вы так настойчиво не хотели скрыть за абстракциями.
Gorthauer87
Сомневаюсь, что если его разнести, он станет понятнее, если там реально сложная бизнес логика, то все равно так или иначе придется в нее въехать, а создание контроллеров, которые всегда тупо переадресуют логику в другой метод это уже какой-то карго культ.
Чтобы разгрести помойку нужны другие средства, чем чистенький код контроллеров.
VolCh
Ну вот в данных примерах контроллер не тупо переадресует, а, средствами фреймворка, преобразует http-запросы во что-то более-менее нейтральное, независимое от протокола.
Gorthauer87
Наверное это как раз адекватный вариант, когда контроллер какое то конкретное представление апи преобрвзует в некоторое обобщенное, не зависящее от реализации.
В таком подходе контроллер точно не будет просто альясить методы в лоб.