Некоторые очень очевидные проблемы с идеей общего IRepository<T>
:
Предполагается, что каждая сущность использует один и тот же тип ключа, что неверно почти в любой нетривиальной системе. Некоторые объекты будут использовать GUID, другие могут иметь какой-то натуральный и / или составной ключ. NHibernate может поддерживать это довольно хорошо, но Linq to SQL довольно плохо справляется с этим - вам нужно написать много хакерского кода для автоматического сопоставления ключей.
Это означает, что каждый репозиторий может иметь дело только с одним типом сущности и поддерживает только самые тривиальные операции. Когда хранилище отправляется в такую простую оболочку CRUD, оно совсем мало используется. Вы также можете просто вручить клиенту IQueryable<T>
или Table<T>
.
Предполагается, что вы выполняете абсолютно одинаковые операции с каждой сущностью. На самом деле это будет очень далеко от истины. Конечно, возможно вы хотите получить этот Order
по его идентификатору, но, скорее всего, вы хотите получить список Order
объектов для конкретного клиента и в некотором диапазоне дат. Понятие полностью универсального IRepository<T>
не учитывает тот факт, что вы почти наверняка захотите выполнять разные типы запросов для разных типов сущностей.
Весь смысл шаблона репозитория заключается в создании абстракции поверх общих шаблонов доступа к данным. Я думаю, что некоторым программистам надоедает создавать репозитории, поэтому они говорят: «Эй, я знаю, я создам один über-репозиторий, который может обрабатывать любой тип сущности!» Это здорово, за исключением того, что репозиторий практически бесполезен для 80% того, что вы пытаетесь сделать. Это хорошо в качестве базового класса / интерфейса, но если это полный объем работы, которую вы делаете, то вы просто ленивы (и гарантируете будущие головные боли).
В идеале я мог бы начать с общего репозитория, который выглядит примерно так:
public interface IRepository<TKey, TEntity>
{
TEntity Get(TKey id);
void Save(TEntity entity);
}
Вы заметите, что у этого нет функции List
или GetAll
- это потому, что абсурдно думать, что допустимо извлекать данные из всей таблицы одновременно в любом месте в код. Это когда вам нужно начать заходить в определенные репозитории:
public interface IOrderRepository : IRepository<int, Order>
{
IEnumerable<Order> GetOrdersByCustomer(Guid customerID);
IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate);
IPager<Order> GetOrdersByProduct(int productID);
}
И так далее - вы поняли идею. Таким образом, у нас есть «общий» репозиторий, если нам когда-нибудь понадобится невероятно упрощенная семантика извлечения по идентификатору, но в целом мы никогда не собираемся передавать это, конечно, не классу контроллера.
Теперь, что касается контроллеров, вы должны сделать это правильно, в противном случае вы в значительной степени отрицаете всю работу, которую вы только что сделали, собирая все репозитории.
Контроллер должен взять свой репозиторий из внешнего мира . Причина, по которой вы создали эти репозитории, заключается в том, что вы можете сделать какую-то инверсию контроля. Ваша конечная цель заключается в том, чтобы иметь возможность поменять один репозиторий на другой - например, выполнить модульное тестирование или если вы решите переключиться с Linq на SQL на Entity Framework в какой-то момент в будущем.
Пример этого принципа:
public class OrderController : Controller
{
public OrderController(IOrderRepository orderRepository)
{
if (orderRepository == null)
throw new ArgumentNullException("orderRepository");
this.OrderRepository = orderRepository;
}
public ActionResult List(DateTime fromDate, DateTime toDate) { ... }
// More actions
public IOrderRepository OrderRepository { get; set; }
}
Другими словами, Контроллер не знает, как создать хранилище , и не должен этого делать. Если у вас там есть какая-то конструкция репозитория, она создает связь, которая вам действительно не нужна. Причина того, что образцы контроллеров ASP.NET MVC имеют конструкторы без параметров, которые создают конкретные репозитории, заключается в том, что сайты должны иметь возможность компилировать и запускать, не вынуждая настраивать всю инфраструктуру внедрения зависимостей.
Но на производственном сайте, если вы не передаете зависимость от репозитория через конструктор или открытое свойство, тогда вы напрасно тратите время на репозитории, потому что контроллеры все еще тесно связаны со слоем базы данных. Вы должны быть в состоянии написать тестовый код, подобный этому:
[TestMethod]
public void Can_add_order()
{
OrderController controller = new OrderController();
FakeOrderRepository fakeRepository = new FakeOrderRepository();
controller.OrderRepository = fakeRepository; //<-- Important!
controller.SubmitOrder(...);
Assert.That(fakeRepository.ContainsOrder(...));
}
Вы не можете сделать это, если ваш OrderController
отключается и создает свой собственный репозиторий. Этот метод тестирования не предназначен для доступа к данным, он просто гарантирует, что контроллер вызывает правильный метод хранилища на основе действия.
Это еще не DI, заметьте, это просто притворство / издевательство. DI приходит на ум, когда вы решаете, что Linq to SQL недостаточно для вас, и вы действительно хотите использовать HQL в NHibernate, но вам понадобится 3 месяца, чтобы портировать все, и вы захотите иметь возможность сделайте это одно хранилище за один раз. Так, например, используя DI Framework, например Ninject , все, что вам нужно сделать, это изменить это:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>();
Bind<IProductRepository>().To<LinqToSqlProductRepository>();
Кому:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();
И вот, теперь все, что зависит от IOrderRepository
, использует версию NHibernate, вам нужно было всего лишь изменить одну строку кода в отличие от потенциально сотен строк. И мы работаем над версиями Linq для SQL и NHibernate бок о бок, портируя функциональность по частям, не нарушая ничего посередине.
Итак, подведем итог всем сделанным мной пунктам:
Не полагайтесь строго на общий IRepository<T>
интерфейс. Большая часть функциональности, которую вы хотите получить из репозитория, специфична , а не универсальная . Если вы хотите включить IRepository<T>
на верхних уровнях иерархии классов / интерфейсов, это нормально, но контроллеры должны зависеть от определенных репозиториев, чтобы вам не пришлось менять свой код в 5 в разных местах, когда вы обнаружите, что в общем хранилище отсутствуют важные методы.
Контроллеры должны принимать репозитории извне, а не создавать свои собственные. Это важный шаг в устранении связи и улучшении тестируемости.
Обычно вам нужно подключить контроллеры с помощью инфраструктуры Dependency Injection, и многие из них могут быть легко интегрированы с ASP.NET MVC. Если это слишком много для вас, то по крайней мере вы должны использовать какой-то статический поставщик услуг, чтобы вы могли централизовать всю логику создания хранилища. (В долгосрочной перспективе вам, вероятно, будет проще просто изучить и использовать DI-фреймворк).