Некоторые очень очевидные проблемы с идеей родового IRepository<T>
:
Он предполагает, что каждый объект использует один и тот же тип ключа, который не является истинным практически любой нетривиальной системы. Некоторые объекты будут использовать GUID, другие могут иметь какой-то естественный и/или составной ключ. NHibernate может поддерживать это довольно хорошо, но Linq to SQL довольно плохо в нем - вам нужно написать много хакерского кода для автоматического сопоставления ключей.
Это означает, что каждый репозиторий может обрабатывать только один тип сущности и поддерживает только самые тривиальные операции. Когда репозиторий отнесен к такой простой CRUD-обертке, он очень мало используется. Вы могли бы просто передать клиенту IQueryable<T>
или Table<T>
.
Предполагается, что вы выполняете точно такие же операции над каждой сущностью. На самом деле это будет очень далеко от истины. Конечно, возможно вы хотите получить это Order
по его ID, но, скорее всего, вы хотите получить список 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);
}
И так далее - вы получаете идею. Таким образом, у нас есть «общий» репозиторий, если нам когда-либо понадобится невероятно упрощенная семантика retrieve-by-id, но в целом мы никогда не собираемся передавать это, конечно, не к классу контроллера.
Теперь, как для контроллеров, вы должны сделать это правильно, иначе вы в значительной степени сведены на нет всю работу, которую вы только что сделали в воедино все репозитории.
Контроллер должен взять его репозиторий из внешнего мира. Причина, по которой вы создали эти хранилища, заключается в том, что вы можете сделать какую-то инверсию управления. Ваша конечная цель здесь состоит в том, чтобы иметь возможность менять один репозиторий для другого - например, для модульного тестирования или если вы решите перейти от 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 имеют беззадачные конструкторы, которые создают конкретные репозитории, заключается в том, что сайты должны быть способны компилироваться и выполняться без принуждения к созданию всей инфраструктуры Injection Dependency.
Но на производственной площадке, если вы не передаете зависимость репозитория через конструктор или общедоступное свойство, вы теряете время, располагая репозиториями, потому что контроллеры по-прежнему тесно связаны с уровнем базы данных , Вы должны быть в состоянии написать тестовый код, как это:
[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>();
To:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();
И там вы, теперь все, что зависит от IOrderRepository
использует версия NHibernate, вам нужно было только изменить одну строку кода в отличие от потенциально сотен строк. И мы запускаем версии Linq to SQL и NHibernate бок о бок, портируя функциональность по частям, не повредив ничего посередине.
Таким образом, чтобы суммировать все моменты, которые я сделал:
Не следует полагаться исключительно на общий IRepository<T>
интерфейс. Большая часть функциональности, которую вы хотите получить в репозитории, составляет , а не generic. Если вы хотите включить IRepository<T>
на верхних уровнях иерархии классов/интерфейсов, это нормально, но контроллеры должны зависеть от конкретных репозиториев, поэтому вам не придется менять код в 5 разных местах, когда вы обнаружите, что в общем хранилище отсутствуют важные методы.
Контроллеры должны принимать репозитории снаружи, а не создавать свои собственные. Это важный шаг в устранении сцепления и улучшении тестируемости.
Обычно вы хотите подключить контроллеры с использованием инфраструктуры Injection Dependency, и многие из них могут быть легко интегрированы с ASP.NET MVC.Если это слишком много для вас, вы должны использовать какой-то статический поставщик услуг, чтобы вы могли централизовать всю логику создания репозитория. (В долгосрочной перспективе вам, вероятно, будет легче просто изучить и использовать инфраструктуру DI).
Общие статические хранилища! УРА! SingletonSquared, просто то, что нам нужно! ;-) –
@ Sky, никогда не может быть достаточно. Я лично думаю о статически частичных общих хранилищах. –