#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
Это тот момент, когда стоит задуматься, какие у вас абстракции и почему. Как у вас хранятся данные на самом деле? Положим, на самом деле вы работаете с голым списком прямо в памяти. В этом случае достаточно принимать Funcfilter и передавать его в Enumerable.Where как есть. Положим, на самом деле вы работаете с базой данных. В этом случае вы можете принимать Expression > filter и передавать его в Queryable.Where как есть. Положим, на самом деле вы работаете с веб-сервисом. В этом случае вы можете принимать список IDictionary с именами свойств и значениями, передавать его сервису, сервис может строить или компилировать соответствующее выражение (в зависимости от того, что в нём на самом деле: вариант 1 или 2). Если требуются более сложные условия, чем проверка на равенство, нужно смотреть, фильтры какой сложности требуется поддерживать, и искать компромиссы. Возможно, сейчас я услышу возражения: "Но мне же нужно супер-универсальное сверх-модульное архи-заменяемое решение!" Нет, оно вам не нужно. YAGNI! Вы никогда не скроете факта, что все три варианта работают с совершенно разной скоростью, имеют разные ограничения и так далее. Нет никакого "общего случая", это миф. Поэтому не изобретайте сложности там, где они вам не нужны. А они вам не нужны практически никогда. Скажите, зачем вам понадобился "сервис", если уже есть "репозиторий"? Вам действительно нужен лишний слой абстракций? Ответ 2
А если понадобится фильтровать по какому либо другому набору полей, не создавать же 100500 таких методов/функций, ведь они могут с собой и пересекаться и тогда явно будет дублирование кода. Отложенное выполнение - то что вам надо. Если сделать вот так, то всегда можно сделать отложенный расчет логики. public IQueryableGetUsers() { 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 IEnumerableGetUsers(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
Комментариев нет:
Отправить комментарий