Рассмотрим простой пример. В качестве исходного набора выберем следующий массив целых чисел: -3, 2, 1, 4, 0, -6, -2, 11, 5, -1, 9, 7, -12. И пусть перед нами стоит задача выбрать из этого набора все нечетные положительные числа в указанном диапазоне. Классическая задача для программиста. Взгляните на условие для конечной выборки, несложно заметить, что оно представляет собой сложный фильтр (фильтр по нескольким критериям), который можно разбить на несколько простых (по каждому из критериев), что мы и сделаем:
- Фильтр нечетных чисел.
- Фильтр положительных чисел.
- фильтр значения попадания в диапазон.
static class FluentFilters
{
// Фильтр нечетных чисел.
public static IQueryable<int> Odd(this IQueryable<int> array)
{
return array.Where(item => item % 2 != 0);
}
// Фильтр положительных чисел.
public static IQueryable<int> Positive(this IQueryable<int> array)
{
return array.Where(item => item > 0);
}
// Фильтр попадания в диапазон.
public static IQueryable<int> Between(this IQueryable<int> array, int left, int right)
{
return array.Where(item => left <= item && item <= right);
}
}
Осталось составить искомый фильтр. Это делается очень легко:
static void Main(string[] args)
{
// Исходный набор.
IQueryable<int> array = new List<int>
{ -3, 2, 1, 4, 0, -6, -2, 11, 5, -1, 9, 7, -12 }
.AsQueryable();
// Применяя каждый фильтр по очереди мы строим план фильтрации.
IQueryable<int> query = array
.Odd()
.Positive()
.Between(2, 9);
// Вот только сейчас происходит фильтрация по плану.
// См метод IQueryProvider.Execute
List<int> result = query.ToList();
//foreach (int i in result)
// Console.WriteLine(i);
//Console.ReadKey();
//
// На консоле увидим:
// 5 9 7
}
Хочу отметить, что применяя данный подход к задаче фильтрации данных можно получить ряд важных преимуществ:
- Расширяемость.
Мы с легкостью можем добавить новый фильтр не затрагивая остальные (например, добавив фильтр на простое число). - Повторное использование.
Мы имеем набор независимых фильтров, которые мы можем повторно использовать для других наборов. - Простота использования и реализации.
Отсутствие громоздких конструкций со сложной структурой.
См. также:
Используем Fluent Builder
Ссылки:
- FluentInterface
- Method Chaining
- Query an ArrayList with LINQ
- Add Custom Methods for LINQ Queries
- Creating an IQueryable LINQ Provider
Хороший способ фильтрации. В мемориз.
ОтветитьУдалитьА почему именно IQueryable? Все отлично ложится и к IEnumerable.
ОтветитьУдалить@gromas
ОтветитьУдалитьСпасибо за комментарий. На самом деле штука настолько простая и очевидная, насколько и полезная и удобная :).
@Sane
ОтветитьУдалитьДело привычки. Да и IQueryable более новый интерфейс, с дополнительными возможностями, нежели IEnumerable.
"Так же хочу обратить Ваше внимание на то, что Fluent filters не должен ассоциироваться только с Linq." - а с чем ещё?
ОтветитьУдалить@inpefess
ОтветитьУдалитьFluent filters - подход, позволяющий придать Вашему коду более читабельный вид и он не привязан ни к конкретному языку, ни к конкретной технологии.
"Make modifier methods return the host object so that multiple modifiers can be invoked in a single expression." (http://martinfowler.com/dslwip/MethodChaining.html)
да, вещь полезная, но надо смотреть по ситуации где ее можно применить, а где нельзя... страдает производительность. если будет массив с несколькими тысячами объектов, то по сути мы будем иметь количество переборов в несколько раз выше, чем надо (конечно, если первый запрос не отсеет практически все :) )... но в целом идея гут! не люблю сложные ифы и прочее, где нифига не понятно "шо это и как оно работает" :)
ОтветитьУдалитьСпасибо за ценный комментарий!
ОтветитьУдалитьСогласен, оптимизация кода и изящество кода вещи не всегда совместимые)
Вот, а по быстродействию, если Вы используете IQueryable, то все упрется в реализацию IQueryProvider, тот самый что по Expression строит IEnumerator.
А так да, быстродействие будет зависеть от порядка применения фильтров: чем больше фильтр отсекает, тем раньше он должен идти.
очень напоминает шаблон спецификация
ОтветитьУдалить@Arseny
ОтветитьУдалитьДа, так как паттерн "Спецификация" построен на основе fluent interface.
http://en.wikipedia.org/wiki/Specification_pattern
"А так да, быстродействие будет зависеть от порядка применения фильтров: чем больше фильтр отсекает, тем раньше он должен идти."
ОтветитьУдалитьПоэтому и нужно использовать IQueryable вместо IEnumerable, так как, для IQueryable QueryProvider выполнит построенную цепочку запросов при необходимости, при этом еще и оптимизирует запрос перед материализацией объектов. При этом фильтры можно применять в порядке удобочитаемости ))
т.е код
var result = FooCollection
.Skip(1000)
.Where(x => x.A > 0)
.Where(x => x.B.Any())
.Take(5)
.ToList();
Выполнится одним оптимальным запросом для IQueryable, а в случае IEnumerable это вызовет запрос и материализацию по меньшей мере 1000 Foo, т.д. по цепочке.
Если QueryProvider достаточно умен, то он конечно проведет оптимизацию, а так кто его знает какой там правайдер заложен, поэтому лучше явно задать порядок.
ОтветитьУдалить