Страницы

Поиск по вопросам

воскресенье, 1 декабря 2019 г.

Как создавать перегруженные методы/функции в сервисе для репозитория

#c_sharp #net #проектирование


Предположим имеется следующий класс:

public class User
{
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public bool Gender {get;set;}
    public int Age {get;set;}
}

public UserService
{
    readonly IRepository _userRepository;

    public UserService(IRepository userRepository)
    {
        _userRepository = userRepository;
    }

    //Функция возвращающая всех пользователей
    public IEnumerable GetUsers()
    {
        return _userRepository.Table.ToList();
    }
}


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

public IEnumerable GetUsers(int age, bool gender)
{
    return _userRepository.Table
        .Where(x=>x.Age==age&&x.Gender==gender)
        .ToList();
}


А если понадобится фильтровать по какому либо другому набору полей, не создавать
же 100500 таких методов/функций, ведь они могут с собой и пересекаться и тогда явно
будет дублирование кода.

Данные User, UserService приведены для примера, все совпадения с действительностью
не специальны.

Хочется узнать как необходимо создавать перегруженные методы/функции.
    


Ответы

Ответ 1



Это тот момент, когда стоит задуматься, какие у вас абстракции и почему. Как у вас хранятся данные на самом деле? Положим, на самом деле вы работаете с голым списком прямо в памяти. В этом случае достаточно принимать Func filter и передавать его в Enumerable.Where как есть. Положим, на самом деле вы работаете с базой данных. В этом случае вы можете принимать Expression> filter и передавать его в Queryable.Where как есть. Положим, на самом деле вы работаете с веб-сервисом. В этом случае вы можете принимать список IDictionary с именами свойств и значениями, передавать его сервису, сервис может строить или компилировать соответствующее выражение (в зависимости от того, что в нём на самом деле: вариант 1 или 2). Если требуются более сложные условия, чем проверка на равенство, нужно смотреть, фильтры какой сложности требуется поддерживать, и искать компромиссы. Возможно, сейчас я услышу возражения: "Но мне же нужно супер-универсальное сверх-модульное архи-заменяемое решение!" Нет, оно вам не нужно. YAGNI! Вы никогда не скроете факта, что все три варианта работают с совершенно разной скоростью, имеют разные ограничения и так далее. Нет никакого "общего случая", это миф. Поэтому не изобретайте сложности там, где они вам не нужны. А они вам не нужны практически никогда. Скажите, зачем вам понадобился "сервис", если уже есть "репозиторий"? Вам действительно нужен лишний слой абстракций?

Ответ 2



А если понадобится фильтровать по какому либо другому набору полей, не создавать же 100500 таких методов/функций, ведь они могут с собой и пересекаться и тогда явно будет дублирование кода. Отложенное выполнение - то что вам надо. Если сделать вот так, то всегда можно сделать отложенный расчет логики. public IQueryable GetUsers() { return _userRepository.Table.AsQueryable(); } Стоит учитывать, что иногда для запроса GetUsers().Where(u => u.Age > 20) можно спокойно вызывать Sum\Count\Show, а иногда лучше явно материализовать запрос (добавить .ToList()), чтобы не дергать базу несколько раз. Для частых кейсов всегда можно завернуть в функцию: public static IQueryable GetActiveUsers() { return GetUsers().Where(u => u.IsActive); } либо просто фильтр: public static IQueryable WhereUsersActive(IQueryable users) { return users.Where(u => u.IsActive); } можно ещё экстеншном, если хочется: public static IQueryable WhereUsersActive(this IQueryable users) { return users.Where(u => u.IsActive); }

Ответ 3



Репозиторий и сервис не нужны. Вы не должны пытаться заранее определить, какие запросы к данным вам понадобятся, да это и не получится. Тысяча функций на все случаи жизни в репозитории — чистый дубляж кода, они обычно не несут особого смысла. IEnumerable или IQueryable — достаточно мощная абстракция. Вы сможете (относительно) легко поменять базу данных. (Проблемы «протекающих абстракций» всё равно останутся — например, если новая база плохо делает какие-нибудь операции.) И вы сможете легко выразить производные операции, отталкиваясь от списка, LINQ вполне подходит для этого. Уточнение: IQueryable — абстракция уровня модели. То, что необходимо выставлять UI-уровню из VM, это не IQueryable<Т>, а коллекция VM-объектов (для вашего случая — UserVM). Скорее всего, вам необходимо будет при переходе к VM-объектам ограничить (пользователю никогда не хочется видеть миллион юзеров, он столько в голове не удержит) и материализовать список сущностей, а не выставлять голый queryable.Select(user => new UserVM(user)), иначе каждый чих в UI будет дёргать базу.

Ответ 4



Ответ на самом деле зависит от того, что подразумевается под "сервисом". Если под сервисом подразумевается что-то, что будет вызываться изнутри бизнес логики - то да, в этом виде он абсолютно излишен. Единственный плюс в таком "сервисе" - он, возможно, чуть-чуть облегчает юнит-тестирование, ценой отказа от IQueryable. Но если под сервисом подразумевается фасад BL-уровня, т.е. вызываться методы этого сервиса будут из UI/Presentation, то сервис в таком виде должен существовать: он прячет IQueryable от Presentation, не давая бизнес-логике выползти в код UI. он должнен, по возможности преобразовывать модели BL в Local DTO - модели, отвязанные от базы, и пригодные для представления. он должен очерчивать время жизни бизнес-операций (контролировать время жизни контекста / unit of work) - этому сейчас мешает лишний Generic Repository. он должен контролировать fetch plan - не давать бизнес-логике уйти в n+1. Предложения выбросить фасад (сервис) в таком случае - это, практически, предложение делать запросы в базу напрямую из UI, что является чистейшим злом :) Посмотрите официальный гайд Microsoft Application Architecture Guide, раздел Business Layer Guidelines. Паттерн, который вы пытаетесь реализовать сервисом, называется Application Façade, его предназначение Centralize and aggregate behavior to provide a uniform service layer. Т.е. он делает именно то, что делает ваш сервис - прячет от UI особенности работы бизнес-логики. Он прячет сам факт существования базы, возможности фильтровать пользователей, особенности выборки - и оставляет тупой интерфейс для UI - отдать параметры, получить пользователей. И в таком случае - ответ на вопрос "как создавать методы" - достаточно очевиден: Интерфейс фасада должен соответствовать потребностям вызывающего его кода UI. А не следовать возможностям фильтрации или каким-то особенностям бизнес операций. Если UI нужно выбрать пользователя по последней букве фамилии - у вас в фасаде должен быть метод "ВыбратьПоПоследнейБуквеФамилии". Если UI не нужен метод "получить всех пользователей" - то и в фасаде его быть не должно.

Ответ 5



Можно вынести параметр делегата фильтрации из Where в параметр функции как то так: public IEnumerable GetUsers(Func filter) { return _userRepository.Table .Where(filter) .ToList(); } и использовать как var users = GetUsers(x=>x.Age==age&&x.Gender==gender); P.S. Если в целом Руководство по C# --- Перегрузка методов

Ответ 6



Возвращать "голые" IEnumerable<> или IQueryable<> "не есть хорошо", поскольку вы сливаете потребителю вашего класса всё и пусть он сам разбирается с этим. Этим вы нарушаете разделение на слои вашего приложения, поскольку в этом случае возвращаемое вами значение может быть проброшено далеко вверх по слоям. Но, как всегда для этого есть решение. Называется "Спецификация": https://en.wikipedia.org/wiki/Specification_pattern Придуман данный шаблон Эриком Эвансом и подробно описан в его книге. Вот ещё пример реализации: http://www.codeproject.com/Articles/670115/Specification-pattern-in-Csharp

Комментариев нет:

Отправить комментарий