Ограничивая абстракции (.NET, ASP.NET MVC)

в 21:31, , рубрики: .net, asp.net mvc, repository, Программирование, Проектирование и рефакторинг

Ограничивая абстракции (.NET, ASP.NET MVC)
Прошло почти три года с тех пор как я впервые написал о своём отказе от такой абстракции как репозиторий (Repository). С тех пор я практически не использовал никаких концепций репозитория в системах, которые мы разрабатываем. Я не убирал из проектов уже существующие репозитории, но теперь я просто не нахожу в них никакой ценности в качестве абстракций.

Репозитории, которые создают разработчики, в основном бывают двух видов:

  • Абстракции вокруг ORM-фреймворка,
  • Инкапсуляция запросов.

Примером первого случая может быть что-нибудь вроде этого:

public interface IConferenceRepository
{
    IRavenQueryable<Conference> Query();
    Conference Load(Guid id);
}

Инкапсуляция запросов обычно занимает несколько больше строк:

public interface IConferenceRepository
{
    IEnumerable<Conference> FindAll();
    IEnumerable<Conference> FindFuture();
    IEnumerable<Conference> FindFree();
    IEnumerable<Conference> FindPaid();
}

Здесь каждый метод инкапсулирует один запрос. Оба случая представляют ценность в определённых сценариях. Если у меня цель — абстрагироваться от моей ORM, я пойду первым путём, и, возможно, включу второй тоже.

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

Копаем глубже

В первую очередь мы должны вернуться к вопросу, для чего мы стали использовать шаблон репозиторий? Наверняка это было сделано во имя «тестируемости». Тогда давайте начнём с чего-то подобного:

public ActionResult Index()
{
	RavenQueryStatistics stats;
	var posts = RavenSession.Query<Post>()
		.Include(x => x.AuthorId)
		.Statistics(out stats)
		.WhereIsPublicPost()
		.OrderByDescending(post => post.PublishAt)
		.Paging(CurrentPage, DefaultPage, PageSize)
		.ToList();

	return ListView(stats.TotalResults, posts);
}

Кажется сложным? Нет. Хотя если сложность будет расти, мы всё ещё будем ограничивать её масштаб одним этим методом. Если мы выведем этот запрос в отдельный класс, репозиторий или метод расширения (extension method), сам запрос всё равно останется в одном методе. С точки зрения метода контроллера, имеет ли значение, где этот код находится – в контроллере или другом классе?

Как насчёт более сложного примера:

public ActionResult Archive(int year, int? month, int? day)
{
	RavenQueryStatistics stats;
	var postsQuery = RavenSession.Query<Post>()
		.Include(x => x.AuthorId)
		.Statistics(out stats)
		.WhereIsPublicPost()
		.Where(post => post.PublishAt.Year == year);

	if (month != null)
		postsQuery = postsQuery.Where(post => post.PublishAt.Month == month.Value);

	if (day != null)
		postsQuery = postsQuery.Where(post => post.PublishAt.Day == day.Value);

	var posts = 
		postsQuery.OrderByDescending(post => post.PublishAt)
		.Paging(CurrentPage, DefaultPage, PageSize)
		.ToList();

	return ListView(stats.TotalResults, posts);
}

Опять, это просто набор запросов. Я всё ещё хочу инкапсулировать это в одном месте, но я не вижу причин перемещать этот код оттуда, где он уже сейчас. Если запрос поменяется, я просто поменяю код в одном месте. Дополнительная абстракция в этом случае может только сбить с толку.

Нюанс возникает в случае, если у меня несколько концепций, с которыми я работаю в методе контроллера. Давайте посмотрим на метод контроллера, который должен делать несколько вещей:

[ValidateInput(false)]
[HttpPost]
public ActionResult Comment(CommentInput input, int id, Guid key)
{
	var post = RavenSession
		.Include<Post>(x => x.CommentsId)
		.Load(id);

	if (post == null || post.IsPublicPost(key) == false)
		return HttpNotFound();

	var comments = RavenSession.Load<PostComments>(post.CommentsId);
	if (comments == null)
		return HttpNotFound();

	var commenter = RavenSession.GetCommenter(input.CommenterKey);
	if (commenter == null)
	{
		input.CommenterKey = Guid.NewGuid();
	}

	ValidateCommentsAllowed(post, comments);
	ValidateCaptcha(input, commenter);

	if (ModelState.IsValid == false)
		return PostingCommentFailed(post, input, key);

	TaskExecutor.ExcuteLater(new AddCommentTask(input, Request.MapTo<AddCommentTask.RequestValues>(), id));

	CommenterUtil.SetCommenterCookie(Response, input.CommenterKey.MapTo<string>());

	return PostingCommentSucceeded(post, input);
}

В этом случае присутствует много валидации, но настоящая работа отдана объекту AddCommentTask. Это объект-команда, которая позаботится о выполнении задачи вне MVC, валидаций, ActionResult и тому подобное.

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

Стратегии тестирования

Моя стратегия тестирования на сегодняшний день это:

  • Юнит-тестирование изолированных компонентов (доменные модели и другие уже изолированные классы)
  • Интеграционное тестирование всего остального

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

Для чего-то вроде баз данных мои тесты будут медленнее. И я предпочитаю принять это, потому что это даёт мне лёгкость при рефакторинге. Мои тесты не ломаются только потому, что какой-то стаб надо переделать.

В своих контроллерах я просто предпочту иметь интерфейс (seam, шов — прим. ред.) для тестирования. В проекте RaccoonBlog это означает, что простой заменой механизма хранения RavenDB на in-memory сделает мои тесты намного быстрее.

Но даже в противном случае – я не беспокоюсь добавлении репозитория. По моему опыту, введение репозитория только для того, чтобы вынести что-то наружу – потеря времени. Это добавляет ненужную абстракцию в том месте, где было бы достаточно какой-то концепции (например, инкапсулирования объекта запроса).

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

Jimmy Bogard – архитектор в компании Headsrping, создатель AutoMapper и соавтор книги ASP.NET MVC in Action. В своём блоге он фокусируется на DDD, CQRS, распределенных системах и сопряжённых архитектурах и методологиях.

Автор: bitmap

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js