Что лучше?Есть сложная логика поиска в хранилище или в службе уровня домена (через IQueryable или другие)? - PullRequest
10 голосов
/ 05 февраля 2011

Мне нужно иметь возможность искать аккаунты клиентов по нескольким полям поиска.Прямо сейчас у меня есть моя логика поиска в моем хранилище.Логика поиска включает в себя некоторую фильтрацию, которая больше похожа на принадлежность к уровню домена, но это будет означать использование чего-то вроде IQueryable, и я не уверен, что мне это тоже нравится.

Например, сейчас у меня есть класс поиска, который имеет все поля, по которым пользователь может искать:

public class AccountSearch
{
    public decimal Amount { get; set; }
    public string CustomerId { get; set; }
    public string Address { get; set; }
    public string CustomerName { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
    public string State { get; set; }
}

Затем у меня есть служба уровня домена, которая просто проходит поисккласс в хранилище.Мне это не нравится:

public class AccountsService : IAccountsService
{
    private readonly IAccountRepository _accountRepository;

    public AccountsService(IAccountRepository accountRepository)
    {
        _accountRepository = accountRepository;            
    }

    public IEnumerable<Account> Search(AccountSearch accountSearch)
    {
        return _accountRepository.Search(accountSearch);
    }
}

И затем, у меня есть вся логика фильтрации в моей реализации репозитория:

public class AccountRepository : IAccountRepository 
{
    private AccountDataContext _dataContext;

    public AccountRepository(AccountDataContext entityFrameworkDataContext)
    {
        _dataContext = entityFrameworkDataContext;
    }

    public IEnumerable<Account> Search(AccountSearch accountSearch)
    {
        // My datacontext contains database entities, not domain entities. 
        // This method must query the data context, then map the database 
        // entities to domain entities.

        return _dataContext.Accounts
            .Where(TheyMeetSearchCriteria)
            .Select(MappedAccounts);
    } 

    // implement expressions here:
    // 1. TheyMeetSearchCriteria filters the accounts by the given criteria
    // 2. MappedAccounts maps from database to domain entities
}

Не уверен, должен ли я чувствовать себя хорошо в этом илиЯ должен найти другой способ реализовать поиск, как это.Что бы вы сделали в этой ситуации?

Ответы [ 2 ]

6 голосов
/ 05 февраля 2011

Существует несколько методов, которые вы можете использовать, лучший из которых будет зависеть от вашего конкретного сценария.

Вместо того, чтобы просто обсуждать логику поиска с точки зрения местоположения (например, в службе или в домене), может быть более полезно провести различие между местоположением спецификации и местоположением исполнения. Под расположением спецификаций я подразумеваю, в каких слоях вы указываете, в каких полях вы хотите искать. Под местом исполнения я подразумеваю немедленное или отложенное исполнение.

Если у вас есть несколько взаимоисключающих типов поиска (т. Е. В сценарии A, который вы хотите искать по CustomerId, а в сценарии B вы хотите искать по CustomerName), это может быть достигнуто путем создания репозитория для конкретного домена с выделенными методами. для каждого типа поиска или в .Net вы можете использовать выражение LINQ. Например:

доменный метод поиска:

_customers.WithName("Willie Nelson")

LINQ-запрос к хранилищу, реализующему IQueryable:

_customers.Where(c => c.Name.Equals("Willie Nelson")

Первый допускает более выразительную область, в то время как последний обеспечивает большую гибкость использования с немного сокращенным временем разработки (возможно, за счет читабельности).

Для более сложных критериев поиска вы можете использовать описанную вами методику передачи набора критериев поиска (строго типизированных или иным образом) или использовать шаблон спецификации . Преимущество шаблона спецификации заключается в том, что он обеспечивает более выразительный, богатый доменом язык запросов. Одним из примеров использования может быть:

_customers.MeetingCriteria(
        Criteria.LivingOutsideUnitedStates.And(Criteria.OlderThan(55)))

Композиция, предоставляемая через шаблон спецификации, может предоставляться также через LINQ API .Net, но с меньшим контролем над указанием кода, раскрывающего намерения.

Что касается времени выполнения, репозитории могут быть написаны для обеспечения отложенного выполнения, возвращая IQueryable или позволяя передавать выражения LINQ для оценки методом репозитория. Например:

Отложенный запрос:

var customer =  (from c in _customers.Query()
                     where c.Name == "Willie Nelson"
                     select c).FirstOrDefault();

Выполняется методом Query ():

var customer =
   _customers.Query(q => from c in q
                           where c.Name == "Willie Nelson"
                           select c).FirstOrDefault();

Прежний метод Query (), который возвращает IQueryable, имеет преимущество, заключающееся в том, что его немного проще тестировать, поскольку Query () может быть легко вставлен в заглушку, чтобы обеспечить работу коллекции, вызываемую кодом, тогда как последний имеет преимущество в том, что детерминированный.

===== EDIT ====

Вдохновленный подходом Гаарона, я решил изменить свой ответ с помощью аналогичной техники. Его подход является своего рода перевернутым шаблоном спецификации, где спецификация выполняет фактический запрос. По сути, это делает запрос самостоятельным, поэтому давайте просто назовем его так:

public class SomeClass
{
    // Get the ICustomerQuery through DI
    public SomeClass(ICustomerQuery customerQuery)
    {
        _customerQuery = customerQuery;
    }

    public void SomeServiceMethod()
    {
        _customerQuery()
            .WhereLivingOutSideUnitedStates()
            .WhereAgeGreaterThan(55)
            .Select();
    }
}

Итак, где вы можете спросить хранилище? Нам здесь не нужен Наш ICustomerQuery может быть просто внедрен с IQueryable, который может быть реализован как вам угодно (возможно, регистрация IoC, которая просто возвращает следующее для NHibernate:

 _container.Resolve<ISession>().Linq<Customer>()
3 голосов
/ 05 февраля 2011

Почему бы вам не выставить IQueryable из самого хранилища?Это позволило бы выполнить любой запрос LINQ из запрашивающего кода.

public class AccountRepository : IAccountRepository 
{
    AccountContext context = new AccountContext ();

    public IQueryable<Account> GetItems ()
    {
        return context.Accounts;
    } 
}

Вы можете сделать AccountSearch ответственным за построение запроса в соответствии с его собственной логикой:

public class AccountSearch
{
    public decimal Amount { get; set; }
    public string CustomerId { get; set; }
    public string Address { get; set; }
    public string CustomerName { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
    public string State { get; set; }

    public IQueryable<Account> BuildQuery (IQueryable<Account> source)
    {
        var query = source.Where (a =>
            a.Amount == Amount);

        // you can use more twisted logic here, like applying where clauses conditionally
        if (!string.IsNullOrEmpty (Address))
            query = query.Where (a =>
               a.Address == Address);

        // ...

        return query;     
    }
}

Тогдаиспользуйте его из кода клиента:

var filter = GetSearchFields (); // e.g. read from UI
var allItems = repository.GetItems ();

var results = filter.BuildQuery (allItems).ToList ();

Это только один из возможных подходов, но мне он нравится, потому что он допускает сложную логику в классе фильтра поиска.Например, у вас может быть переключатель в пользовательском интерфейсе с различными типами поиска, которые в свою очередь выполняют поиск по разным полям.Это все выражается в AccountSearch при использовании этого шаблона.Вы также можете сделать некоторые поля поиска необязательными, как я сделал с Address в этом примере.В конце концов, вы берете на себя ответственность за фактическое построение запроса из клиентского кода в AccountSearch, который лучше всего подходит для него, потому что он лучше знает об условиях поиска и их значении.

...