Ограничивая абстракции (.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, распределенных системах и сопряжённых архитектурах и методологиях.

ссылка на оригинал статьи http://habrahabr.ru/post/161703/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *