Repository and Specification pattern
I'm currently setting up a new project, and I have run into a few things, where I need a little input.
This is what i'm considering:
I would like a generic repository
I don't want to return IQueryable from my repository.
I would like to encapsulate my queries in specifications.
I have implemented the specification pattern
It needs to be easily testable
Now this is where I get a little stuck and my question is which way would be the most elegant way of calling the find method with one or more specifications:
(Fluent): bannerRepository.Find().IsAvailableForFrontend().IsSmallMediaBanner()
или выражать запросы как лямбды с моими спецификациями
(Лямбда): bannerRepository.Find.Where(banner => banner.IsFrontendCampaignBanner && banner.IsSmallMediaBanner)
или может быть каким-то совершенно другим способом? Самое главное, чтобы парень, реализующий фронт MVC, имел хороший интуитивный опыт работы с хранилищем.
Чего я надеюсь достичь, так это сохранить некоторую гибкость в отношении возможности сочетания спецификаций и предоставления опыта "фильтрации" со спецификациями, но без утечки IQueryable для контроллера, но, скорее, для ISpecifiable, который позволяет только изменить запрос со спецификациями, а не с Linq. Но я только что вернулся к утечке логики запросов в контроллер таким образом?
3 ответа
Я видел некоторые Fluent API, которые используют свойства для спецификаций, поэтому они не добавляют шум круглых скобок для клиентов.
bannerRepository.Find.IsAvailableForFrontend.IsSmallMediaBanner.Exec()
Быть Exec() метод для выполнения спецификаций в отношении репо.
но даже если вы не используете свойства, я бы пошел на свободный API, так как он имеет минимальный шум.
или может быть каким-то совершенно другим способом?
Ну, на самом деле я не получаю именно вашу реализацию репозитория (например, что будет метод .Find()
вернуть?), но я бы выбрал другое направление:
public class Foo
{
public Int32 Seed { get; set; }
}
public interface ISpecification<T>
{
bool IsSatisfiedBy(T item);
}
public interface IFooSpecification : ISpecification<Foo>
{
T Accept<T>(IFooSpecificationVisitor<T> visitor);
}
public class SeedGreaterThanSpecification : IFooSpecification
{
public SeedGreaterThanSpecification(int threshold)
{
this.Threshold = threshold;
}
public Int32 Threshold { get; private set; }
public bool IsSatisfiedBy(Foo item)
{
return item.Seed > this.Threshold ;
}
public T Accept<T>(IFooSpecificationVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
public interface IFooSpecificationVisitor<T>
{
T Visit(SeedGreaterThanSpecification acceptor);
T Visit(SomeOtherKindOfSpecification acceptor);
...
}
public interface IFooRepository
{
IEnumerable<Foo> Select(IFooSpecification specification);
}
public interface ISqlFooSpecificationVisitor : IFooSpecificationVisitor<String> { }
public class SqlFooSpecificationVisitor : ISqlFooSpecificationVisitor
{
public string Visit(SeedGreaterThanSpecification acceptor)
{
return "Seed > " + acceptor.Threshold.ToString();
}
...
}
public class FooRepository
{
private ISqlFooSpecificationVisitor visitor;
public FooRepository(ISqlFooSpecificationVisitor visitor)
{
this.visitor = visitor;
}
public IEnumerable<Foo> Select(IFooSpecification specification)
{
string sql = "SELECT * FROM Foo WHERE " + specification.Accept(this.visitor);
return this.DoSelect(sql);
}
private IEnumerable<Foo> DoSelect(string sql)
{
//perform the actual selection;
}
}
Итак, у меня есть объект, его интерфейс спецификации и несколько разработчиков, участвующих в шаблоне посетителя, его интерфейс репозитория, принимающий интерфейс спецификации, и его реализация репозитория, принимающая посетителя, способного переводить спецификации в предложения SQL (но это только вопрос этого случая, конечно). Наконец, я бы составил спецификацию "за пределами" интерфейса репозитория (используя свободный интерфейс).
Может быть, это просто наивная идея, но я нахожу ее довольно простой. Надеюсь это поможет.
Лично я бы пошел с лямбда-пути. Это может быть из-за моей любви к лямбде, но она предоставляет много места для общей настройки репозитория.
Учитывая следующее:
bannerRepository.Find.Where(banner => banner.IsFrontendCampaignBanner && banner.IsSmallMediaBanner)
Я не знаю, как выглядит ваш шаблон, но вы могли бы изменить некоторые вещи здесь:
Создайте универсальный интерфейс с именем "IRepository" типа, содержащий все методы для доступа к данным.
Это может выглядеть так:
interface IRepository<T> where T : class
{
IEnumerable<T> FindAll(Func<T, bool> exp);
T FindSingle(Func<T, bool> exp);
}
Создайте абстрактный класс 'Repository', реализующий этот интерфейс:
class Repository<T> : IRepository<T> where T : class
{
TestDataContext _dataContext = TestDataContext();
public IEnumerable<T> FindAll(Func<T, bool> exp)
{
_dataContext.GetTable<T>().Where<T>(exp);
}
public T FindSingle(Func<T, bool> exp)
{
_dataContext.GetTable<T>().Single(exp);
}
}
Теперь мы можем создать интерфейс для таблицы / объектов баннеров, который реализует наш 'IRepository' и конкретный класс, расширяющий абстрактный класс 'Repository' и реализующий 'IBannerInterface':
interface IBannerRepository : IRepository<Banner>
{
}
И соответствующий репозиторий для его реализации:
class BannerRepository : Repository<Banner>, IBannerRepository
{
}
Я бы предложил использовать этот подход, поскольку он дает вам большую гибкость и достаточную мощность для управления всеми крошечными объектами, которые у вас есть.
Вызов этих методов будет таким простым:
BannerRepository _repo = new BannerRepository();
_repo.FindSingle(banner => banner.IsFrontendCampaignBanner && banner.IsSmallMediaBanner);
Да, это означает, что вам нужно проделать определенную работу, но в дальнейшем вам будет гораздо проще изменить источник данных.
Надеюсь, поможет!