Главная страница   /   12.1. Зачем нужны облегченные контроллеры (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

12.1. Зачем нужны облегченные контроллеры

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

Простота поддержки

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

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

Легкое тестирование

Лучший способ гарантировать легкую работу с исходным кодом - это разработка через тестирование (test-driven development, TDD). Когда мы используем технику TDD, мы работаем с нашим исходным кодом прежде, чем он написан. Тяжелые для тестирования классы, в том числе контроллеры, сразу же помечаются как неработоспособные.

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

Сфокусированность на одной обязанности

Быстрый способ облегчить загрузку контроллера – уменьшить количество его обязанностей. Рассмотрим следующее усложненное действие:

Листинг 12-1: Усложненный контроллер
public RedirectToRouteResult Ship(int orderId)
{
	User user = _userSession.GetCurrentUser();
	Order order = _repository.GetById(orderId);
	if (order.IsAuthorized)
	{
		ShippingStatus status = _shippingService.Ship(order);
		if (!string.IsNullOrEmpty(user.EmailAddress))
		{
			Message message = _messageBuilder
					.BuildShippedMessage(order, user);
			_emailSender.Send(message);
		}
		if (status.Successful)
		{
			return RedirectToAction("Shipped", "Order", new { orderId });
		}
	}
	return RedirectToAction("NotShipped", "Order", new { orderId });
}

Строка 5: Проверяет, может ли заказ быть отправлен

Строка 8: Проверяет, нужно ли отправить email

Это действие делает слишком много, и его невозможно понять с первого взгляда. Можно посчитать его обязанности по количеству утверждений if. Помимо своей положенной роли - управления пользовательскими интерфейсами - это действие решает, готов ли заказ Order для отправки, и следует ли отправить пользователю User уведомление по электронной почте. Кроме того, оно также решает, как это сделать. Данное действие определяет, что это значит, когда Order готов для погрузки, и как именно должно быть отправлено уведомление по электронной почте.

Логика вроде этой – доменная и бизнес-логика - не должна находиться в классе пользовательского интерфейса, таком как контроллер. Это нарушает SRP, затеняет истинные намерения домена и реальные обязанности контроллера - перенаправлять к соответствующему действию. Такие приложения трудно тестировать и поддерживать.

Принцип единственной обязанности (single responsibility principle, SRP)

Суть принципа единственной обязанности (SRP) в том, что класс должен быть небольшим и сфокусированным. По сути, SRP означает, что класс должен иметь одну и только одну обязанность. Если посмотреть с другой стороны, то этот принцип означает, что должна быть только одна причина изменить класс. Если вы обнаружите, что есть причины для изменения класса, не связанные с его основной задачей, то это скорее всего значит, что класс делает слишком много. Обычно принцип SRP нарушается, когда доступ к данным смешивается с бизнес-логикой. Так, например, класс Customer не должен включать метод Save().

SRP является ключевой концепцией хорошего объектно-ориентированного дизайна, и его применение облегчает поддержку кода. SRP иногда называют разделением обязанностей (separation of concerns, SoC). Больше о SRP/SoC можно прочитать в отличной статье Боба Мартина " SRP: принцип единственной обязанности» (http://mng.bz/34TU).

Упростить эту ситуацию можно с помощью простого рефакторинга на слои Refactor Architecture by Tiers. Он позволяет разработчикам переместить логику обработки из уровня представления на бизнес-уровень. Вы можете узнать больше об этой технике на страничке Мартина Фаулера, посвященной рефакторингам: http://www.refactoring.com/catalog/refactorArchitectureByTiers.html.

Когда мы переместили логику отправки заказа в OrderShippingService, наше действие стало гораздо проще.

public RedirectToRouteResult Ship(int orderId)
{
	var status = _orderShippingService.Ship(orderId);
	if (status.Successful)
	{
		return RedirectToAction("Shipped", "Order", new { orderId });
	}
	return RedirectToAction("NotShipped", "Order", new { orderId });
}

Мы переместили все, что связано с отправкой заказа и уведомления, из контроллера в новый класс OrderShippingService. У контроллера осталась одна обязанность - решить, куда перенаправить клиента. Новый класс будет обрабатывать и Order, и User, и все остальное.

Но результат рефакторинга - больше, чем простое перемещение. Это семантический разрыв, который возлагает ответственность за управление этими задачами на нужные классы. Это изменение создает чистую абстракцию, которую наш контроллер может использовать для репрезентации того, что раньше было его обязанностями. Другие контроллеры также могут использовать OrderShippingService. Новая абстракция проста, и она может меняется внутри, не влияя на презентации использующие ее.

Цикломатическая сложность: вязкость исходного кода

Цикломатическая сложность является метрикой для анализа сложности кода. Чем больше логических путей представляет метод или функция, тем выше ее цикломатическая сложность. Чтобы полностью понять последствия тех или иных процедур, необходимо оценить каждый логический путь. Например, каждое простое утверждение if представляет два пути – один, когда условие истинно, и другой, когда оно ложно. Функции с высокой цикломатической сложности более сложны для тестирования и понимания, а также ведут увеличению количества ошибок.

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