Последнее изменение: 1 сентября 2007г.

Библиотека фильтрации

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

По большому счету, библиотека тривиальна. Она содержит определение фильтра, фильтруемого объекта и четырех операций над фильтрами – AND, OR, NOT и XOR. Эти операции позволяют создать логику любой сложности. В принципе, можно было бы обойтись и без XOR, она добавлена для удобства.

Это описание делится на две части:

Сразу хочу предупредить. Библиотека написана с использованием generics, т.е. может использоваться только с Java 5.0+.

* * *

Описание библиотеки

Итак, посмотрим на библиотеку поближе.

Фильтруемый объект определяется интерфейсом ru.skipy.filtering.Filterable. Этот интерфейс пустой, по сути – маркер.

Функциональность фильтра определяется интерфейсом ru.skipy.filtering.Filter. Он приведен ниже (все комментарии опущены, полный вариант есть в исходном коде):

package ru.skipy.filtering;

public interface Filter <T extends Filterable> {

    public boolean accept(T element);

    public String getName();

    public String getDescription();

    public String getHumanreadableRule();
}

Основной метод интерфейса – accept. Полагаю, комментарии тут излишни, его назначение более чем понятно. Кроме него в интерфейсе есть еще три метода. getName и getDescription в большой степени предназначены для использования в интерфейсе пользователя. Они возвращают, соответственно, имя и описание фильтра. Последний метод, getHumanreadableRule, должен возвращать правило фильтрации в виде, нонятном человеку. Этот метод может быть очень полезен при создании сложных фильтров с помощью операций – классы, реализующие операции, используют его для создания полного правила фильтрации. Реализацию и применение всех этих методов можно найти в примере, который находится вместе с библиотекой в виде исходников.

Для тех, кто не знаком с generics, пара слов об объявлении интерфейса. Выражение <T extends Filterable> означает возможность параметризации. Параметром-типом является тип T, который должен быть наследником интерфейса Filterable. Как вы можете заметить, тот же тип фигурирует и в объявлении метода accept. Это означает, что реализующий этот интерфейс разработчик может объявить действительный тип, тем самым получив его в методе accept:

public class MyData implements Filterable{
    //...
}

public class MyFilter implements Filter<MyData>{

    public boolean accept(MyData element){
        // ...
    }

    // ...
}

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

Перейдем теперь к операциям над фильтрами. Как я уже упоминал, их реализовано четыре – AND, OR, NOT и XOR. В принципе, XOR можно было не реализовывать, ибо

a xor b = (a or b) and not(a and b)

Однако на практике это означало бы создание сложного фильтра (4-х экземпляров интерфейса Filter), при том, что реализация его тривиальна.

Самая простая операция – NOT. Она реализована классом ru.skipy.filtering.operations.NotOp. Конструктор этого класса принимает единственный параметр – фильтр, значение которого он будет инвертировать.

Далее по сложности – операция XOR. Она реализована классом ru.skipy.filtering.operations.XorOp. В конструкторе у нее два параметра – фильтры, над которыми будет произведена операция. Хочу напомнить: xor возвращает false при равных аргументах и true при неравных.

Далее идут операции AND и OR (соответственно, ru.skipy.filtering.operations.AndOp и ru.skipy.filtering.operations.OrOp). Эти операции могут работать одновременно с несколькими фильтрами, причем все фильтры связаны одним и тем же отношением операции.

Каждый из реализующих операции классов имеет два конструктора. Один принимает набор фильтров (java.util.List<Filter<T>>), второй – два фильтра. Набор фильтров не может быть пустым, фильтры не могут быть равны null. Впрочем, все это есть в javadoc.

Фильтры применяются в порядке их указания в наборе (либо первый, потом второй фильтры при использовании конструктора с двумя фильтрами). Важно помнить, что фильтры в целях оптимизации производительности применяются до получения определенности. Иначе говоря, если какой-либо из вложенных фильтров в операции AND вернул false, то результат уже определен, соответственно, остальные фильтры не применяются. Аналогично действует и возврат вложенным фильтром значения true в операции OR.

Собственно, это всё. Пример реализации и применения фильтров есть в тесте, который находится в архиве вместе с исходным кодом.

* * *

Исходный код

Исходный код библиотеки находится здесь – filtering.zip. Он поставляется вместе с build.xml для ant. По умолчанию ant компилирует библиотеку и создает файл filtering.jar в директории lib. Документация генерируется в директории doc с помощью команды ant javadoc, тест запускается с помощью команды ant run-test. Тест весьма и весьма тривиален, он лишь демонстрирует создание и использование сложного фильтра.

Обратите внимание, что для корректной работы ant необходимо правильно установить переменную JAVA_HOME. Она должна указывать на директорю, в которой установлен J2SDK 5.0.

Всё. Пользуйтесь!

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