Статьи

Функциональный стиль в Java с предикатами. Часть 1

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

Что такое предикат?

Я действительно влюбился в предикаты, когда впервые обнаружил коллекции Apache Commons , давно, когда я программировал на Java 1.4. Предикат в этом API — это не что иное, как интерфейс Java только с одним методом:

1
evaluate(Object object): boolean

Вот и все, он просто берет некоторый объект и возвращает истину или ложь. Более поздним эквивалентом коллекций Apache Commons является Google Guava с лицензией Apache 2.0. Он определяет интерфейс Predicate с помощью одного метода с использованием универсального параметра:

1
apply(T input): boolean

Это так просто. Чтобы использовать предикаты в вашем приложении, вам просто нужно реализовать этот интерфейс с вашей собственной логикой в ​​едином методе apply (что-то) .  

Простой пример

В качестве первого примера представьте, что у вас есть список заказов объектов PurchaseOrder , каждый из которых имеет дату, клиента и состояние. Различные варианты использования, вероятно, потребуют, чтобы вы выяснили каждый заказ для этого клиента, или каждый ожидающий, отправленный или доставленный заказ, или каждый заказ, выполненный с последнего часа. Конечно, вы можете сделать это с помощью циклов foreach и, если внутри, таким образом:

01
02
03
04
05
06
07
08
09
10
11
//List<PurchaseOrder> orders...
 
public List<PurchaseOrder> listOrdersByCustomer(Customer customer) {
  final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>();
  for (PurchaseOrder order : orders) {
    if (order.getCustomer().equals(customer)) {
      selection.add(order);
    }
  }
  return selection;
}

И снова для каждого случая:

1
2
3
4
5
6
7
8
9
public List<PurchaseOrder> listRecentOrders(Date fromDate) {
  final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>();
  for (PurchaseOrder order : orders) {
    if (order.getDate().after(fromDate)) {
      selection.add(order);
    }
  }
  return selection;
}

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

1
2
3
4
5
6
7
8
9
public List<PurchaseOrder> listOrders(<strong>Predicate<PurchaseOrder> condition</strong>) {
  final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>();
  for (PurchaseOrder order : orders) {
    if (condition.apply(order)) {
      selection.add(order);
    }
  }
  return selection;
}

Каждый конкретный предикат может быть определен как отдельный класс, если он используется в нескольких местах, или как анонимный класс:

1
2
3
4
5
6
final Customer customer = new Customer("BruceWaineCorp");
final Predicate<PurchaseOrder> condition = new Predicate<PurchaseOrder>() {
  public boolean apply(PurchaseOrder order) {
    return order.getCustomer().equals(customer);
  }
};

Ваши друзья, которые используют настоящие функциональные языки программирования (Scala, Clojure, Haskell и т. Д.), Прокомментируют, что приведенный выше код ужасно многословен, чтобы сделать что-то очень распространенное, и я должен согласиться. Однако мы привыкли к этому многословию в синтаксисе Java, и у нас есть мощные инструменты (автозаполнение, рефакторинг), чтобы приспособиться к нему. И наши проекты, вероятно, в любом случае не смогут переключиться на другой синтаксис в одночасье.  

Предикаты коллекции лучших друзей

Возвращаясь к нашему примеру, мы написали цикл foreach только один раз, чтобы охватить все варианты использования, и мы были довольны этим факторингом. Однако ваши друзья, занимающиеся функциональным программированием «по-настоящему», могут все еще смеяться над этим циклом, который вы должны были написать сами. К счастью, и API от Apache, и Google также предоставляют все полезные свойства, которые вы можете ожидать, в частности, класс, похожий на java.util.Collections , следовательно, с именем Collections2 (не очень оригинальное имя).

Этот класс предоставляет метод filter (), который делает нечто похожее на то, что мы написали ранее, поэтому теперь мы можем переписать наш метод без цикла:

1
2
3
public Collection<PurchaseOrder> selectOrders(Predicate<PurchaseOrder> condition) {
  return Collections2.filter(orders, condition);
}

Фактически, этот метод возвращает фильтрованное представление:

Возвращенная коллекция представляет собой живое представление unfiltered (входная коллекция); изменения одного влияют на другое.

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

При аналогичном подходе, при наличии итератора, вы можете запросить фильтрованный итератор поверх него (шаблон Decorator), который дает вам только элементы, выбранные вашим предикатом:

1
Iterator filteredIterator = Iterators.filter(unfilteredIterator, condition);

Начиная с Java 5 интерфейс Iterable очень удобен для использования с циклом foreach, поэтому мы бы предпочли использовать следующее выражение:

1
2
3
4
5
6
7
8
public Iterable<PurchaseOrder> selectOrders(Predicate<PurchaseOrder> condition) {
  return Iterables.filter(orders, condition);
}
 
// you can directly use it in a foreach loop, and it reads well:
for (PurchaseOrder order : orders.selectOrders(condition)) {
  //...
}

Готовые предикаты

Чтобы использовать предикаты, вы можете просто определить собственный интерфейс Predicate или один для каждого параметра типа, который вам нужен в вашем приложении. Это возможно, однако хорошая вещь при использовании стандартного интерфейса Predicate от API, такого как Guava или Commons Collections, состоит в том, что API предоставляет множество отличных строительных блоков для объединения с вашими собственными реализациями предикатов.

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

1
2
3
4
5
6
// gives you a predicate that checks if an integer is zero
Predicate<Integer> isZero = Predicates.equalTo(0);
// gives a predicate that checks for non null objects
Predicate<String> isNotNull = Predicates.notNull();
// gives a predicate that checks for objects that are instanceof the given Class
Predicate<Object> isString = Predicates.instanceOf(String.class);

Учитывая предикат, вы можете инвертировать его (true становится ложным, и наоборот):

1
Predicates.not(predicate);

Объедините несколько предикатов, используя логические операторы AND, OR:

1
2
3
4
Predicates.and(predicate1, predicate2);
Predicates.or(predicate1, predicate2);
// gives you a predicate that checks for either zero or null
Predicate<Integer> isNullOrZero = Predicates.or(isZero, Predicates.isNull());

Конечно, у вас также есть специальные предикаты, которые всегда возвращают true или false, которые действительно очень полезны, как мы увидим позже для тестирования:

1
2
Predicates.alwaysTrue();
Predicates.alwaysFalse();

Где найти ваши предикаты

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

Кстати, где найти эти предикаты? После Роберта К. Мартина и его Общего принципа закрытия (КПК) :

Классы, которые меняются вместе, принадлежат друг другу

Поскольку предикаты манипулируют объектами определенного типа, мне нравится размещать их рядом с типом, который они принимают в качестве параметра. Например, классы CustomerOrderPredicate , PendingOrderPredicate и RecentOrderPredicate должны находиться в том же пакете, что и класс PurchaseOrder, который они оценивают, или в подпакете, если их много. Другой вариант — определить их вложенные в сам тип. Очевидно, что предикаты достаточно связаны с объектами, с которыми они работают.

Ресурсы

Вот исходные файлы для примеров в этой статье: cyriux_predicates_part1 (zip)

В следующей части мы рассмотрим, как предикаты упрощают тестирование, как они соотносятся со спецификациями в доменно-ориентированном проектировании, а также некоторые дополнительные материалы, позволяющие извлечь максимальную пользу из ваших предикатов.

Справка: легкая функциональность в простой Java с предикатами — часть 1 от нашего партнера по JCG