ASP.NET контроллеры должны быть тонкими
Ох уж эта вечно повторяемая банальность, обросшая тоннами недосказанности.
Почему они должны быть тонкими? Какой в этом плюс? Как сделать их тонкими, если они сейчас не такие? Как сохранить их тонкими?
Правильные (и частые) вопросы. Обсуждение части этих вопросов можно найти в моих ранних статьях, поэтому сейчас мы посмотрим на проблему с другой стороны.
Чтобы начать шаги по превращению наших контроллеров в тонкие, сначала необходимо понять, как контроллеры становятся толстыми.
По моему опыту, есть 6 основных видов кода, проникающих в наши контроллеры, хотя им там вовсе не место. На самом деле этот список не исчерпывающий, и я уверен, что их ещё больше.
1. Маппинг объектов передачи данных (DTO)
Так как наши контроллеры находятся на передовой процесса обработки запроса, часто возникает необходимость создания объектов для запроса и ответа, если нужно получить что-то сложнее, чем просто параметры из адреса, и вернуть не только код ответа HTTP.
Вы понимаете, это что-то вроде:
public IActionResult CheckOutBook([FromBody]BookRequest bookRequest)
{
var book = new Book();
book.Title = bookRequest.Title;
book.Rating = bookRequest.Rating.ToString();
book.AuthorID = bookRequest.AuthorID;
//...
}
Логика маппинга довольно невинна, однако, она очень быстро раздувает контроллер до неприличных размеров и добавляет ему новую ответственность. В идеальном случае единственной ответственностью нашего контроллера должно быть проксирование запроса с уровня HTTP вглубь приложения и передача ответа обратно.
2. Валидация
Конечно же, мы не можем позволить некорректному вводу пробраться внутрь стен нашего замка, и валидация защищает нас от этого. Сначала на стороне клиента, а затем и на сервере.
Однако, мне нравится относиться к контроллерам как к шеф-поварам. Их ассистенты подготавливают все ингредиенты, поэтому сами они занимаются только финальной компоновкой. И существует множество способов настройки валидаторов в процессе обработки запроса ASP.NET MVC, чтобы контроллеры могли считать запрос валидным и передавать его дальше по цепочке.
Вот такому коду нет оправданий!
public IActionResult Register([FromBody]AutomobileRegistrationRequest request)
{
// Проверяем, что VIN номер был заполнен...
if (string.IsNullOrEmpty(request.VIN))
{
return BadRequest();
}
//...
}
3. Бизнес-логика
Если у вас есть что-то, относящееся к бизнесу в ваших контроллерах, то скорее всего вам понадобится написать то же самое где-то ещё.
Иногда это пересекается и с валидацией тоже. Если в вашей валидации есть правила, продиктованные бизнесом (а не просто проверка принадлежности числа диапазону или наличия строки), то велик риск, что этот код будет повторён в другом месте.
4. Авторизация
Авторизация похожа на валидацию в том плане, что представляет собой защитный барьер. Только в отличие от предотвращения попадания плохих запросов в систему, авторизация не даёт плохим пользователям попасть туда, куда им не следует.
Аналогично в случае с валидацией, ASP.NET предлагает множество путей для выноса авторизации (ПО промежуточного слоя и фильтры, например).
Если вы проверяете свойства объекта User
внутри контроллера, чтобы разрешить/запретить ему что-то, то, кажется, что у вас есть кое-что для рефакторинга.
5. Обработка ошибок
Это больно, это БОЛЬНО!
public IActionResult GetBookById(int id)
{
try
{
// Важный код, который должен выполнять шеф-повар...
}
catch (DoesNotExistException)
{
// Код, который должен выполнять ассистент...
}
catch (Exception e)
{
// Пожалуйста, только не это...
}
}
Этот пункт довольно обширный, и иногда обработка исключений должна быть сделана в контроллере, но, по моему опыту, почти всегда есть более подходящее, более локализованное место. И если такого места нет, то вы можете воспользоваться преимуществами глобальной обработки исключений на промежуточном слое, чтобы отловить наиболее общие ошибки и вернуть что-то адекватное клиентам.
Возвращаясь к метафоре про шеф-поваров, я предпочитаю не беспокоить своих шеф-поваров подобными задачами. Дайте им делать то единственное, за что они ответственны и предполагать, что кто-то другой обработает непредвиденные ситуации.
6. Сохранение/получение данных
Часто в целях экономии времени в контроллерах появляется код, получающий или сохраняющий объекты, используя Репозитории
. Если контроллер предоставляет только CRUD операции, то к чёрту, пускай.
У меня даже есть статья, показывающая использование контроллеров таким образом.
Вместо того, чтобы просто называть это плохим поведением, думаю, пример альтернативного способа продемонстрирует, почему это может раздуть ваши контроллеры сверх необходимого.
Для начала взглянем на это с архитектурной точки зрения (фокусируясь на Принципе Единой Ответственности). Если ваши контроллеры используют объекты, предназначенные для хранения данных, то у контроллеров есть явно больше одной причины для изменения.
public IActionResult CheckOutBook(BookRequest request)
{
var book = _bookRepository.GetBookByTitleAndAuthor(request.Title, request.Author);
// Если у вас уже есть логика получения книги, то вы скорее
// всего захотите добавить сюда и логику выдачи этой книги
// ...
return Ok(book);
}
За пределами базовых CRUD операций, логика работы с репозиториями является плохим запахом, сигнализирующем о коде, который было бы лучше спрятать поглубже в цепочке обработки запроса.
Именно здесь мне нравится использовать некий сервис (спрятанный за интерфейсом) для обработки запроса или делегирования какому-нибудь CQRS объекту.
На этом всё!
Статья закончилась, а у вас есть ещё примеры, которые не были освещены? Не согласны с каким-то из пунктов? Или просто хотите задать вопрос? Добро пожаловать в комментарии!
Перевод статьи подготовлен в преддверии старта курса "C# ASP.NET Core разработчик".
Всех, кто желает подробнее узнать о курсе и программе обучения, приглашаю записаться на день открытых дверей, который я проведу уже 5 мая.
UPD:
Так как в переводимой статье нет чётких ответов на 4 вопроса, заданных в начале, дополняем её этими ответами здесь. И для начала ответим на «Почему они должны быть тонкими?» и «Какой в этом плюс?».
Плюсы тонких контроллеров в их читаемости, тестируемости, простоте поддержки и переиспользовании кода, вынесенного в сервис, вместо копипасты в разных контроллерах. Самым же главным я считаю то, что у контроллера должна быть только одна ответственность — работа с запросами на HTTP уровне. То есть, получить HTTP запрос и сделать вызов кода на обработку запроса, а также выбрать правильный HTTP код и заголовки для ответа в зависимости от результатов обработки.
Ответами же на вопросы «Как сделать их тонкими, если они сейчас не такие?» и «Как сохранить их тонкими?» как раз и занимается текущая статья — не нужно допускать лишний код в контроллеры, а если он там есть, то выносить.