Статьи

Коллекции, встречайте Expression!

В моем текущем проекте мы используем  много  сборов данных. И, как и в большинстве программных проектов, наиболее распространенные (хотя и далеко не исключительные) операции находят элемент из коллекции и что-то с ним делают.

Как разработчик стереотипных Java, я также привык писать простые и понятные решения. То, что читается и легко понимается всеми. Я также очень ленивый человек, когда пишу код, поэтому я не хочу повторяться. Вот почему я часто использую API Apache Collections, чтобы сделать мою жизнь немного проще. Для тех, кто не знает эту библиотеку, она содержит набор очень полезных типов и функций, которые помогают освободить вас от написания этого кода. Позвольте мне объяснить:

Представьте, что у вас есть коллекция под названием «яблоки», которая содержит кучу объектов Apple. Если мы хотим найти первое зеленое яблоко из этой связки, мы могли бы написать что-то вроде этого:

CollectionUtils.find ( apples, isGreenApple );

Переменная «greenApples» определяется как реализация интерфейса Predicate. Этот интерфейс очень прост: он содержит только один метод (valuate), который возвращает логическое значение. Его единственная цель — определить, соответствует ли предоставленный объект предикату или нет. Мы можем определить предикат isGreenApple из нашего предыдущего примера следующим образом:

Predicate isGreenApple = new Predicate () {
   public boolean evaluate(Apple apple) {
      return apple.isGreen();
   } 
}

Коллекции Apache содержат гораздо больше возможностей, чем просто поиск объектов в коллекции, но пока мы ограничимся этой концепцией.

Преимущества работы с коллекцией, как это должно стать ясно со временем:

  • Мы разделяем заботу об операции над коллекцией откуда.
  • Это позволит нам часто использовать наши предикаты независимо от того, что мы хотим с ними делать.

Но есть и некоторые побочные эффекты:

  • Этот синтаксис очень многословен.
  • На самом деле, в большом программном проекте повторное использование подобных предикатов практически отсутствует. Трудно найти и повторно использовать предикаты.

Я думаю, что мы можем сделать лучше, чем это …

Предикаты выражений

По сути, мы хотим описать, как идентифицировать и возражать, а затем что с этим делать. И, конечно, это должно быть легко читать и работать с ним. Мы уже выяснили часть «что с этим делать», а не как ее идентифицировать.

На момент написания, лямбда-выражения не доступны для нас. По крайней мере, в индустрии Java пока не представляется возможным перейти на Java 8, чтобы мы могли получить доступ к использованию лямбда-выражений. Так что это еще не вариант.

Но в мире Java есть спецификация, которую мы могли бы сделать. Он легко читается, прост в использовании и очень мощный. Мы используем ему  много  в рамках таких как JSF, WebFlow и т.д. Это называется Единый язык выражений.

Условие из нашего предыдущего примера может быть легко переписано как выражение EL:

«Apple.isGreenApple ()» или «apple.greenApple»

Так почему бы не использовать это?

Мы можем построить нашу собственную реализацию Predicate и использовать библиотеку EL для разрешения выражения для нас. Наш ExpressionPredicate может выглядеть примерно так:

public class ExpressionPredicate<T> implements Predicate<T> {

    private ContextMap contextMap;
    private String objectName;
    private ELContext context;

    private ValueExpression expression;
    
    public ExpressionPredicate(String objectName, String expression) {
        this(objectName, expression, Maps.<String, Object>newHashMap());
    }
    
    public ExpressionPredicate(String objectName, String expression, Object expectedResult, Map<? extends String, ? extends Object> variableMap) {
        this.contextMap = new ContextMap(variableMap);
        this.objectName = objectName;
        
        CompositeELResolver compositeELResolver = createELResolver(contextMap);
        this.context = createContext(compositeELResolver);
        
        ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
        this.expression = expressionFactory.createValueExpression(
                this.context, expression, expectedResult.getClass());
    }

    @Override
    public boolean evaluate(T object) {
        contextMap.assignBaseVariable(objectName, object);
        return Boolean.valueOf( evaluateExpression(context) );
    }

    Object evaluateExpression(ELContext context) {
        return this.expression.getValue(context);
    }
    
    private CompositeELResolver createELResolver(ContextMap contextMap) {
        CompositeELResolver compositeELResolver = new CompositeELResolver();
        compositeELResolver.add(contextMap);
        compositeELResolver.add(new ArrayELResolver());
        compositeELResolver.add(new ListELResolver());
        compositeELResolver.add(new BeanELResolver());
        compositeELResolver.add(new MapELResolver());
        return compositeELResolver;
    }

    ELContext createContext(final ELResolver elResolver) {
        return new ELContext() {
            @Override
            public VariableMapper getVariableMapper() {
                return new VariableMapperImpl();
            }
            @Override
            public FunctionMapper getFunctionMapper() {
                return new FunctionMapperImpl();
            }
            @Override
            public ELResolver getELResolver() {
                return elResolver;
            }
        };
    }

    static class ContextMap extends MapELResolver {
        Map<String, Object> context;
    
        public ContextMap(Map<? extends String, ? extends Object> context) {
            this.context = Maps.<String, Object>newHashMap(context);
        }

        @Override
        public Object getValue(ELContext elContext, Object base, Object property) {
            if (base == null) {
                base = context;
            }
            return super.getValue(elContext, base, property);
        }
        
        public void assignBaseVariable(String objectName, Object objectValue) {
            context.put(objectName, objectValue);
        }
    }

}

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

  • Мы настроили преобразователь языка выражений, чтобы мы могли оценить выражение
  • Мы  привязываем  «контекстную карту» к резольверу, который будет содержать всю нашу переменную, когда мы ее оценим
  • Перед выполнением каждой оценки мы помещаем текущий объект в «контекстную карту». ObjectName будет использоваться для идентификации текущего объекта во время нашей итерации.
  • В этом примере мы предполагаем, что выражение оценивается как true или false. Мы анализируем этот результат и возвращаем его после оценки. Обратите внимание, что если оценка не возвращает true, она всегда будет возвращаться к false. Поэтому, если вы хотите поддерживать больше типов возвращаемых данных для своего выражения, вы должны отредактировать приведенный выше пример и добавить поддержку для сравнения «ожидаемого результата», а не предполагать, что возвращаемый результат является логическим.

В приведенном выше примере я использовал реализацию JBoss EL, но есть еще много провайдеров для API javax.el. Я также использую Apache Collections (конечно) и Google Guava.

С этим предикатом мы уже на полпути к тому, чего мы хотим. К настоящему времени мы уже можем сделать это:

CollectionUtils.find (apples, new ExpressionPredicate("apple", "${apple.greenApple}") );

Также обратите внимание, что можно предоставить базовую карту для ExpressionPredicate. Это позволит нам передавать переменные и другие объекты в наше выражение. Представьте себе следующий пример:

Map<string, apple=""> arguments = Maps.newHashMap();
arguments.put("color", "green");
CollectionUtils.find (apples, new ExpressionPredicate("apple", "${apple.colorName eq color}", arguments) );

В приведенном выше коде мы привязываем «зеленую» строку к переменной «color». Выражение является стандартным выражением EL, выполняя равное для свойства «colorName» предоставленной нами переменной «color».

Но мы можем сделать немного лучше …

Синтаксический сахар

Здесь не хватает синтаксического сахара. Мы не всегда хотим называть объект, который мы собираемся перебрать; мы можем просто назвать это просто «вар». Я также хотел бы иметь простой метод, который я могу вызвать, чтобы построить карту переменных. И, наконец, может быть хорошей идеей предусмотреть модель строителя. Наши выражения уже могут быть очень мощными, но мы можем разделить наше выражение на несколько частей. Или, возможно, мы захотим позже построить наш ExpressionPredicate более динамично.

public class ExpressionUtils {
    
    public static final String OBJECT_NAME = "var";
    
    public static <T> ExpressionBuilder<T> newExpression() {
        return new ExpressionBuilder<T>();
    }
    
    public static class ExpressionBuilder<T> {
        
        private List<ExpressionPredicate<T>> expressions;
        
        public ExpressionBuilder() {
            expressions = Lists.newArrayList();
        }
        
        public ExpressionBuilder<T> withProperty(String propertyName) {
            expressions.add(new ExpressionPredicate<T>(OBJECT_NAME, "${" + OBJECT_NAME + "." + propertyName + "}"));
            return this;
        }
        
        public ExpressionBuilder<T> with(String expression) {
            return with(expression, Maps.<String, Object>newHashMap());
        }
        
        public ExpressionBuilder<T> with(String expression, Map<? extends String, ? extends Object> arguments) {
            expressions.add(new ExpressionPredicate<T>(OBJECT_NAME, "${" + expression + "}", arguments));
            return this;
        }

        public Predicate<T> build() {
            return AllPredicate.getInstance( expressions );
        }

    }

    public static Map<? extends String, ? extends Object> newMap(String arg, Object value) {
        return ImmutableMap.<String, Object>of(arg, value);
    }

    public static Map<? extends String, ? extends Object> newMap(String arg0, Object value0, String arg1, Object value1) {
        return ImmutableMap.<String, Object>of(arg0, value0, arg1, value1);
    }
    
    public static Map<? extends String, ? extends Object> newMap(String arg0, Object value0, String arg1, Object value1, String arg2, Object value2) {
        return ImmutableMap.<String, Object>of(arg0, value0, arg1, value1, arg2, value2);
    }

    public static Builder<String, Object> newMap() {
        return ImmutableMap.<String, Object>builder();
    }

    public static <T> Predicate<T> newExpression(String expression, Map<? extends String, ? extends Object> newMap) {
        return new ExpressionBuilder<T>().with(expression, newMap).build();
    }

    public static <T> Predicate<T> newExpression(String expression) {
        return new ExpressionBuilder<T>().with(expression).build();
    }
}

С этим классом утилит / строителей у нас есть очень мощный инструмент, в котором мы можем использовать выражения для выбора элементов из коллекций.

Мы можем построить сложные оценки:

CollectionUtils.find ( apples, ExpressionUtils.newExpression ( "var.greenApple and not var.oldApple" ) );

Мы можем добавить переменные в наши выражения:

CollectionUtils.find ( apples, ExpressionUtils.newExpression ( 
    "var.greenApple and var.brandName eq brand", ExpressionUtils.newMap( "brand", "PinkLady" ) ) );

И мы можем даже построить их динамически, если нам нужно:

ExpressionBuilder expression= ExpressionUtils.newExpression()
     .with ("var.greenApple);
if (excludeOldApples) {
    expression.with("var.oldApple");
}
CollectionUtils.find ( apples, expression.build() );

Там нет серебряной пули

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

Причина в том, что наш компилятор не сможет проверять это выражение для нас. Это большой недостаток. Если мы напишем ошибку в выражении, наш компилятор будет игнорировать ее, пока мы не оценим выражение во время выполнения. Это даже более важно, если мы хотим реорганизовать или переписать наш код: мы не сразу узнаем, сломали ли мы выражение, например, когда изменим имя метода или свойства нашего объекта.

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

Вывод

Библиотека Apache Collections — отличная библиотека для работы с коллекциями. Однако необходимость писать подробные предикаты, которые иногда трудно читать, снижает удобство использования этой библиотеки. Используя Unified Expression Language и создавая реализацию Predicate, мы можем значительно повысить читаемость, гибкость и простоту использования библиотеки Collections. Мы можем представить себе оценку сложных выражений, выражений с привязкой переменных и даже динамическое построение этих выражений во время выполнения. Из-за этого мы значительно уменьшаем объем разрабатываемого кода и повышаем собственную производительность.

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

С Java 8 в настоящее время завершается, мы получим доступ к лямбда-выражениям. В конце концов, когда Lambda станет общепринятым принципом в индустрии Java, мы можем снова рассмотреть возможность «рефакторинга» нашего ExpressionPredicate для использования этих языковых конструкций, что может снова позволить нашему компилятору выполнить проверку во время компиляции.