Суть шаблона подробно описана в статьях P of EAA Catalog: Repository и Domain-Driven Design Community: Repository. Шаблон Repository представляет собой фасад для доступа к данным. Repository реализует механизм для хранения, извлечения и поиска объектов в источнике данных.
Сейчас в большинстве случаев шаблон Repository используется для скрытия конкретной ORM (NHibernate, Entity Framework, Linq2Sql, LLBLGen и т.п.) от остальной части приложения. Наравне с этим он может служить для обёртки любого другого источника данных: сервиса, файловой системы, Active Directory и т.п.
В идеале наша система сохраняет и выбирает доменные объекты из Repository так, как будто это коллекция объектов в памяти. Это позволяет нам полностью игнорировать специфику хранения и выборки объектов.
Проблемы при использовании шаблона Repository
Как любой другой шаблон проектирования, Repository имеет множество интерпретаций и реализаций. Вообще, когда речь идет о проектировании, то сложно отделить абсолютно правильное решение от полностью неправильного. Обычно говорят, что всё зависит от ситуации. С другой стороны, в блогах и статьях попадаются решения, в которых авторы нарушают принципы проектирования или предлагают реализации с явными проблемами. Я собрал такие способы использования шаблона Repository, для того чтобы обратить внимание на возможные проблемы их применения.
Проблема: Repository, как фабрика запросов
Объект IQueryable в .NET дает больше возможностей сделать ошибки в проектировании. К примеру, довольно популярным является такая реализация шаблона Repository:
interface IRepository<TEntity> { IQueryable<TEntity> Query(); // ... } class MyOrmAccountRepository : IRepository<Account> { IQueryable<Account> Query() { // здесь вызов оборачиваемой ORM } // ... }
Похоже, что такая реализация является довольно популярной: Хабр: LINQ to SQL: паттерн Repository, GotDotNet: NHibernate: паттерн Repository, Microsoft Blog: Using Repository Pattern with Entity Framework, MSDN: Persistence Patterns.
К чему может привести передача объекта IQueryable из объекта Repository? Рассмотрим ситуацию выборки пользователей:
var accountRepository = new AccountRepository(); List<Account> activeAccounts = accountRepository.Query() .Where(x => x.IsDeleted == false) .ToList();
Потом ещё где-нибудь в коде:
var accountRepository = new AccountRepository(); var activeAdmins = accountRepository.Query() .Where(x => x.Role == AccountRole.Admin) .Where(x => x.IsDeleted == false) .ToList();
Здесь мы подразумеваем, что в текущих понятиях предметной области активными пользователями являются те, которые не удалены. Причем эти условия x.IsDeleted == false дублируются в разных частях бизнес-логики. Что будет, когда добавится ещё один флаг или состояние у пользователей. Например, флаг IsArchive будет делать пользователей неактивными. Во все выборки пользователей мы должны будем добавить еще одно условие x.IsArchive == false:
var activeAdmins = repository.Query() .Where(x => x.Role == AccountRole.Admin) .Where(x => x.IsDeleted == false && x.IsArchive == false) .ToList();
Теперь понятно, что мы даем пользователям нашего интерфейса IRepository делать запросы какие угодно, где угодно, с дублированием логики. Посмотрим в определение шаблона Repository и станет понятно, что с этой проблемой он был призван бороться: «...adding this layer helps minimize duplicate query logic».
Решение №1
Одним из решений может быть создание специфического метода у объекта Repository, который будет иметь смысловое название. Вот реализация для выборки активных пользователей:
public class AccountRepository { public IEnumerable<Account> GetActiveAccounts() { return ormDataContext.Query<Account>() .Where(x => x.IsDeleted == false && x.IsArchive == false) .ToList(); } }
В такой реализации выборка активных пользователей вне AccountRepository выглядит так:
var accountRepository = new AccountRepository();
var activeAccounts = accountRepository.GetActiveAccount();
Мы локализовали логику определения активного пользователя только в методе GetActiveAccounts. Теперь при добавлении или удалении флагов у объекта Account изменится только метод в AccountRepository.
Решение №2
Можно использовать шаблон Specification вместе c шаблоном Query Object. Второй шаблон уже реализован в .NET с помощью дерева запросов на LINQ.
Реализация для выборки активных пользователей:
public class ActiveAccountSpecification : ISpecification { public Expression<Func<Account, bool>> IsSatisfiedBy() { return x => x.IsDeleted == false && x.IsArchive == false; } } public class AccountRepository { public IEnumerable<Account> Find(ISpecification specification) { return ormDataContext.Query<Account>() .Where(specification.IsSatisfiedBy()) .ToList(); } } // использование спецификации var accountRepository = new AccountRepository(); accountRepository.Find(new ActiveAccountSpecification());
Условия объектов Specification можно накладывать друг на друга. Хороший пример есть в Wikipedia: Specification pattern.
Также, эту тему недавно поднимал menozz в гугл-группе DotNetConf: Многокриториальный поиск и паттерн Specification. Там есть несколько примеров кода.
Проблема: Выход специфики DAL за рамки Repository
Эта проблема относится в основном к ORM, которые генерируют код по существующей структуре БД.
Шаблон Repository в основном используется, как обертка над ORM. Он скрывает специфику построения запросов с помощью ORM, инициализации объектов и т.п. Например, вы получили из AccountRepository объект Account. После этого ставите точку и обращаетесь к сгенерированному полю:
var accountRepository = new AccountRepository(); Account account = accountRepository.GetById(1); // свойство IsChanged сгенерировано ORM if (account.IsChanged) { // ...
Получается, что с помощью Repository мы скрывали источник данных для нашего приложения. В итоге получилось, что используем специфику доступа к данным вне Repository. Более подробно эта проблема описана в статье LLBLGen vs. NHibernate.
Проблема: Один Repository на всех
Попытка создать один универсальный Repository для всех доменных сущностей всегда оборачивается нарушением принципа единственности ответственности и разрастанием этого Repository. Зачастую такие универсальные решения нарушают принцип открытости/закрытости (пример с Repository).
Решение
Нужно разделить реализацию универсального Repository на более маленькие и специфические.
Проблема: Создание Repository для каждого объекта домена
Отдельный объект Repository нужно делать только для корня агрегации, к которому нам нужен непосредственный доступ. Более подробно в статье Domain-Driven Design: aggregation root.
Проблема: Ссылка на Repository из доменного объекта
Ещё одна популярная ошибка, когда в доменном объекте мы можем составлять запросы к БД. Например:
public class Manager { private IEnumerable<customer> customers; public IEnumerable<customer> Customers { get { return customers ?? (customers = new CustomerRepository().Query().Where(x => x.Manager == this).ToList()); } } }
В такое реализации мы наделяем доменный объект Manager дополнительными обязанностями:
- создание CustomerRepository;
- составление запроса для получения всех клиентов данного менеджера.
Это нарушает как минимум принцип единственности ответственности и инверсии зависимости. Как результат получившаяся система будет менее стабильна.
Решение
Очевидно, что создание запроса нужно вынести в ManagerRepository.
Проблема: использование шаблона Repository без шаблона Unit Of Work
Пример:
Account account = accoutRepository.GetById(1);
Product product = new Product();
accoutRepository.Save(product);
account.AddProduct(product);
accoutRepository.Save(account);
Что будет, если запрос на первом Save() выполнится, а на втором нет? Удалять из БД созданный объект product? Этот код демонстрирует то, что мы можем привести БД в несогласованное состояние.
Решение
Шаблон Unit Of Work реализован практически всеми ORM. Работа с данными должна проходить внутри единицы работы. Более подробно с примерами кода описано в Совершенный код №2. Пример реализации Unit Of Work с NHibernate.
Проблема: Связывание Repository и Unit of Work
Один из вариантов связывания Repository с Unit of Work заключается в том, что мы передаем во все объекты Repository ссылку на созданную Unit of Work. Пример реализации в статье Совместное использование Repository с Unit Of Work:
using (IUnitOfWork uow = new MyDataContext()) { ICustomerRepository cr = new EFCustomerRepository(uof); IOrderRepository or = new EFOrderRepository(uof); // CRUD действия с объектами // Завершаем транзакцию. uow.Save(); }
Возникает несколько проблем:
- Каждый Repository получает возможность управлять общей единицей работы. Например, EFCustomerRepository может в любой момент вызвать uow.Save(). Это может привести к ошибкам в логике и нарушает принцип SRP
- Как мы будем создавать ICustomerRepository с помощью инжекции зависимости, если ему в конструктор требуется экземпляр Unit of Work?
- Единица работы может создаваться в контроллере, а Repository в сервисе. Тогда придется передавать ссылку на текущий Unit of Work из контроллера в сервис, из сервиса в Repository
Я бы не рекомендовал напрямую связывать Unit of Work и Repository. Решение проблемы связывания приведено в статье Совершенный код №2. Пример реализации Unit Of Work с NHibernate.
Проблема: Смешивание Repository и Specification
Как я уже говорил, шаблон Repository часто пытаются превратить в нечто универсальное. Вот ещё один пример в статье RepositoryUnit: удобный шаблонный репозиторий.
Всегда нужно стремиться к разделению обязанностей в классах. Чем меньше ваши классы, чем меньше они связаны друг с другом, тем проще вам изменять, тестировать и понимать проект.
Проблема: Смешивание Repository и Unit of Work
Пример ещё одного смешивания. На этот раз Repository и UoW. Пример в статье Entity Framework 4 POCO, Repository and Specification Pattern.
Лучше будет оставлять контроль за транзакцией клиенту объекта Repository, делать этот контроль внешним по отношению к Repository.
Недостаток шаблона Repository
С ростом приложения нам требуется всё больше разных запросов. Объект Repository может вырасти до десятков публичных функций. Нечёткая ответственность делает такой Repository нарушителем SRP.
Ссылки
Domain-Driven Design Community: Repository