Feel Good.

16 апреля 2010

Используем Fluent Filters

В этой статье я хотел бы рассказать про очень элегантное решение получении выборок с помощью Fluent Filters, основная идея которого заключается в построении цепочки методов, называемые фильтрами, для искомого набора данных. Под набором я буду подразумевать объект типа IQueryable (IEnumerable). Цель Fluent Filters - свести процесс создания сложного фильтра к процессу композиции нескольких более простых фильтров, причем композиция фильтров реализована в форме Fluent interface.

Рассмотрим простой пример. В качестве исходного набора выберем следующий массив целых чисел: -3, 2, 1, 4, 0, -6, -2, 11, 5, -1, 9, 7, -12. И пусть перед нами стоит задача выбрать из этого набора все нечетные положительные числа в указанном диапазоне. Классическая задача для программиста. Взгляните на условие для конечной выборки, несложно заметить, что оно представляет собой сложный фильтр (фильтр по нескольким критериям), который можно разбить на несколько простых (по каждому из критериев), что мы и сделаем:

  1. Фильтр нечетных чисел.
  2. Фильтр положительных чисел.
  3. фильтр значения попадания в диапазон.
Отлично, реализуем каждый фильтр в лучших традициях Fluent interface, используя Extension Methods для типа IQueryable:


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

}


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

  1. Расширяемость.
    Мы с легкостью можем добавить новый фильтр не затрагивая остальные (например, добавив фильтр на простое число).
  2. Повторное использование.
    Мы имеем набор независимых фильтров, которые мы можем повторно использовать для других наборов.
  3. Простота использования и реализации.
    Отсутствие громоздких конструкций со сложной структурой.
Так же хочу обратить Ваше внимание на то, что Fluent filters не должен ассоциироваться только с Linq.

См. также:
Используем Fluent Builder

Ссылки:
  1. FluentInterface
  2. Method Chaining
  3. Query an ArrayList with LINQ
  4. Add Custom Methods for LINQ Queries
  5. Creating an IQueryable LINQ Provider


Progg it

12 комментариев:

  1. Хороший способ фильтрации. В мемориз.

    ОтветитьУдалить
  2. А почему именно IQueryable? Все отлично ложится и к IEnumerable.

    ОтветитьУдалить
  3. @gromas

    Спасибо за комментарий. На самом деле штука настолько простая и очевидная, насколько и полезная и удобная :).

    ОтветитьУдалить
  4. @Sane

    Дело привычки. Да и IQueryable более новый интерфейс, с дополнительными возможностями, нежели IEnumerable.

    ОтветитьУдалить
  5. "Так же хочу обратить Ваше внимание на то, что Fluent filters не должен ассоциироваться только с Linq." - а с чем ещё?

    ОтветитьУдалить
  6. @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)

    ОтветитьУдалить
  7. да, вещь полезная, но надо смотреть по ситуации где ее можно применить, а где нельзя... страдает производительность. если будет массив с несколькими тысячами объектов, то по сути мы будем иметь количество переборов в несколько раз выше, чем надо (конечно, если первый запрос не отсеет практически все :) )... но в целом идея гут! не люблю сложные ифы и прочее, где нифига не понятно "шо это и как оно работает" :)

    ОтветитьУдалить
  8. Спасибо за ценный комментарий!

    Согласен, оптимизация кода и изящество кода вещи не всегда совместимые)

    Вот, а по быстродействию, если Вы используете IQueryable, то все упрется в реализацию IQueryProvider, тот самый что по Expression строит IEnumerator.

    А так да, быстродействие будет зависеть от порядка применения фильтров: чем больше фильтр отсекает, тем раньше он должен идти.

    ОтветитьУдалить
  9. очень напоминает шаблон спецификация

    ОтветитьУдалить
  10. @Arseny

    Да, так как паттерн "Спецификация" построен на основе fluent interface.
    http://en.wikipedia.org/wiki/Specification_pattern

    ОтветитьУдалить
  11. "А так да, быстродействие будет зависеть от порядка применения фильтров: чем больше фильтр отсекает, тем раньше он должен идти."

    Поэтому и нужно использовать 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, т.д. по цепочке.

    ОтветитьУдалить
  12. Если QueryProvider достаточно умен, то он конечно проведет оптимизацию, а так кто его знает какой там правайдер заложен, поэтому лучше явно задать порядок.

    ОтветитьУдалить