Ответ 1
Некоторые очень очевидные проблемы с идеей общего 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);
}
И так далее - вы получаете идею. Таким образом, у нас есть "общий" репозиторий, если нам когда-либо понадобится невероятно упрощенная семантика 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.
Но на производственном сайте, если вы не передаете зависимость репозитория через конструктор или общедоступное свойство, вы теряете время, имея репозитории вообще, потому что контроллеры все еще тесно связаны с уровнем базы данных. Вам нужно написать тестовый код следующим образом:
[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>
интерфейс. Большинство функций, которые вы хотите использовать в репозитории, являются специфическими, а не универсальными. Если вы хотите включитьIRepository<T>
на верхних уровнях иерархии классов/интерфейсов, это прекрасно, но контроллеры должны зависеть от конкретных репозиториев, поэтому вам не придется менять код в 5 разных местах, когда вы обнаружите, что в общем хранилище отсутствуют важные методы. -
Контроллеры должны принимать репозитории извне, а не создавать свои собственные. Это важный шаг в устранении связи и улучшении тестируемости.
-
Обычно вам нужно подключить контроллеры, используя инфраструктуру Injection Dependency, и многие из них могут быть легко интегрированы с ASP.NET MVC. Если это слишком много для вас, то, по крайней мере, вы должны использовать какой-то статический поставщик услуг, чтобы вы могли централизовать всю логику создания репозитория. (В долгосрочной перспективе вам, вероятно, будет легче просто изучить и использовать инфраструктуру DI).