![](https://habrastorage.org/storage2/1f0/3da/2b0/1f03da2b0c6dcdd1868d970728ce1ad4.jpg)
Прошло почти три года с тех пор как я впервые написал о своём отказе от такой абстракции как репозиторий (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, распределенных системах и сопряжённых архитектурах и методологиях.