Мы все ищем один и тот же IRepository?
Я пытался придумать способ написать универсальные репозитории, которые работают с различными хранилищами данных:
public interface IRepository
{
IQueryable<T> GetAll<T>();
void Save<T>(T item);
void Delete<T>(T item);
}
public class MemoryRepository : IRepository {...}
public class SqlRepository : IRepository {...}
Я хотел бы работать с теми же классами домена POCO в каждом. Я также рассматриваю аналогичный подход, где у каждого класса домена есть свой собственный репозиторий:
public interface IRepository<T>
{
IQueryable<T> GetAll();
void Save(T item);
void Delete(T item);
}
public class MemoryCustomerRepository : IRepository {...}
public class SqlCustomerRepository : IRepository {...}
Мои вопросы: 1) Возможен ли первый подход? 2) Есть ли преимущество у второго подхода.
3 ответа
Первый подход осуществим, я делал нечто подобное в прошлом, когда писал свою собственную структуру отображения, предназначенную для RDBMS и
XmlWriter
/XmlReader
, Вы можете использовать такой подход для упрощения модульного тестирования, хотя я думаю, что теперь у нас есть превосходные инструменты OSS для этого.Второй подход - это то, что я сейчас использую с сопоставителями IBATIS.NET. Каждый маппер имеет интерфейс, и каждый маппер [мог бы] обеспечить ваши основные операции CRUD. Преимущество каждого картографа для класса домена также имеет определенные функции (такие как
SelectByLastName
или жеDeleteFromParent
), которые выражены интерфейсом и определены в конкретном картографе. Из-за этого мне не нужно реализовывать отдельные репозитории, как вы предлагаете - наши конкретные картографы нацелены на базу данных. Для выполнения модульных тестов я использую StructureMap и Moq для создания репозиториев в памяти, которые работают как вашиMemory*Repository
делает. Его меньше классов для реализации и управления и меньше работы в целом для очень проверяемого подхода. Для данных, общих для модульных тестов, я использую шаблон построителя для каждого класса домена, который имеетWithXXX
методы иAsSomeProfile
методы (AsSomeProfile
просто возвращает экземпляр компоновщика с предварительно сконфигурированными тестовыми данными).
Вот пример того, что я обычно получаю в своих юнит-тестах:
// Moq mocking the concrete PersonMapper through the IPersonMapper interface
var personMock = new Mock<IPersonMapper>(MockBehavior.Strict);
personMock.Expect(pm => pm.Select(It.IsAny<int>())).Returns(
new PersonBuilder().AsMike().Build()
);
// StructureMap's ObjectFactory
ObjectFactory.Inject(personMock.Object);
// now anywhere in my actual code where an IPersonMapper instance is requested from
// ObjectFactory, Moq will satisfy the requirement and return a Person instance
// set with the PersonBuilder's Mike profile unit test data
На самом деле, существует общее мнение, что репозитории доменов не должны быть общими. Ваш репозиторий должен содержать информацию о том, что вы можете делать при сохранении или извлечении ваших сущностей.
Некоторые репозитории доступны только для чтения, некоторые - только для вставки (без обновления, без удаления), некоторые имеют только определенные поиски...
При использовании GetAll, возвращающего IQueryable, ваша логика запросов попадет в ваш код, возможно, на уровень приложения.
Но по-прежнему интересно использовать предоставляемый вами интерфейс для инкапсуляции Linq Table<T>
объекты, чтобы вы могли заменить его реализацией в памяти для целей тестирования.
Поэтому я предлагаю назвать это ITable<T>
Дай тот же интерфейс что и linq Table<T>
объект, и используйте его в ваших конкретных хранилищах домена (не вместо).
Затем вы можете использовать свои конкретные репозитории в памяти, используя в памяти ITable<T>
реализация.
Самый простой способ реализовать ITable<T>
в памяти, чтобы использовать List<T>
и получить IQueryable<T>
интерфейс с использованием метода расширения.AsQueryable().
public class InMemoryTable<T> : ITable<T>
{
private List<T> list;
private IQueryable<T> queryable;
public InMemoryTable<T>(List<T> list)
{
this.list = list;
this.queryable = list.AsQueryable();
}
public void Add(T entity) { list.Add(entity); }
public void Remove(T entity) { list.Remove(entity); }
public IEnumerator<T> GetEnumerator() { return list.GetEnumerator(); }
public Type ElementType { get { return queryable.ElementType; } }
public IQueryProvider Provider { get { return queryable.Provider; } }
...
}
Вы можете работать отдельно от базы данных для тестирования, но с действительно конкретными репозиториями, которые дают больше информации о домене.
Это немного поздно... но взглянем на реализацию IRepository в CommonLibrary.NET на codeplex. У него довольно хороший набор функций.
Что касается вашей проблемы, я вижу много людей, использующих методы, такие как GetAllProducts(), GetAllEmployees() в своей реализации репозитория. Это избыточно и не позволяет вашему хранилищу быть универсальным. Все, что вам нужно, это GetAll() или All(). Решение, предоставленное выше, решает проблему именования.
Это взято из документации CommonLibrary.NET онлайн:
0.9.4 Beta 2 имеет мощную реализацию репозитория.
* Supports all CRUD methods ( Create, Retrieve, Update, Delete )
* Supports aggregate methods Min, Max, Sum, Avg, Count
* Supports Find methods using ICriteria<T>
* Supports Distinct, and GroupBy
* Supports interface IRepository<T> so you can use an In-Memory table for unit-testing
* Supports versioning of your entities
* Supports paging, eg. Get(page, pageSize)
* Supports audit fields ( CreateUser, CreatedDate, UpdateDate etc )
* Supports the use of Mapper<T> so you can map any table record to some entity
* Supports creating entities only if it isn't there already, by checking for field values.