Статьи

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

В первой части этой статьи мы представили предикаты, которые приносят некоторые преимущества функционального программирования для объектно-ориентированных языков, таких как Java, через простой интерфейс с одним единственным методом, который возвращает истину или ложь. Во второй и последней части мы рассмотрим некоторые более сложные понятия, чтобы извлечь максимальную пользу из ваших предикатов.
тестирование
Один очевидный случай, когда предикаты сияют, это тестирование. Всякий раз, когда вам нужно протестировать метод, который смешивает обход структуры данных и некоторую условную логику, используя предикаты, вы можете тестировать каждую половину изолированно, сначала обходя структуру данных, а затем условную логику.
На первом шаге вы просто передаете в метод предикат всегда истинный или всегда ложный, чтобы избавиться от условной логики и сосредоточиться только на правильном переходе по структуре данных:
1
2
3
4
5
6
// check with the always-true predicate
final Iterable<PurchaseOrder> all = orders.selectOrders(Predicates.<PurchaseOrder> alwaysTrue());
assertEquals(2, Iterables.size(all));
 
// check with the always-false predicate
assertTrue(Iterables.isEmpty(orders.selectOrders(Predicates.<PurchaseOrder> alwaysFalse())));
На втором шаге вы просто тестируете каждый возможный предикат отдельно.
1
2
3
final CustomerPredicate isForCustomer1 = new CustomerPredicate(CUSTOMER_1);
assertTrue(isForCustomer1.apply(ORDER_1)); // ORDER_1 is for CUSTOMER_1
assertFalse(isForCustomer1.apply(ORDER_2)); // ORDER_2 is for CUSTOMER_2
Этот пример прост, но вы поняли идею. Чтобы проверить более сложную логику, если тестирования каждой половины функции недостаточно, вы можете создать фиктивные предикаты, например, предикат, который возвращает true один раз, а затем всегда false. Подобные предикаты могут значительно упростить настройку тестов благодаря строгому разделению задач.
Предикаты работают так хорошо для тестирования, что, если вы склонны использовать некоторые TDD, я имею в виду, что если способ тестирования может повлиять на ваш дизайн, то, как только вы узнаете предикаты, они обязательно найдут свой путь в ваш дизайн.
Объясняя команде
В проектах, над которыми я работал, команда сначала не была знакома с предикатами. Однако эта концепция достаточно проста и интересна для всех, чтобы быстро ее освоить. На самом деле я был удивлен тем, как идея предикатов естественным образом распространилась из кода, который я написал в код моих коллег, без особой евангелизации от меня. Я предполагаю, что преимущества предикатов говорят сами за себя. Наличие зрелых API от таких известных компаний, как Apache или Google, также помогает убедить, что это серьезная вещь. И теперь, когда шумиха по поводу функционального программирования, продавать стало еще проще!
Простые оптимизации
Обычная оптимизация состоит в том, чтобы сделать предикаты как можно более неизменными и не сохраняющими состояния, чтобы обеспечить их совместное использование без учета потоков. Это позволяет использовать один единственный экземпляр для всего процесса (как одиночный, например, как статические конечные константы). Наиболее часто используемые предикаты, которые не могут быть перечислены во время компиляции, могут кэшироваться во время выполнения, если это необходимо. Как обычно, делайте это только в том случае, если ваш отчет профилировщика действительно требует этого.
Когда это возможно, объект предиката может предварительно вычислить некоторые из вычислений, участвующих в его оценке, в своем конструкторе (естественно, поточно-ориентированном) или лениво.
Ожидается, что предикат не будет иметь побочных эффектов , другими словами «только для чтения»: его выполнение не должно вызывать каких-либо заметных изменений в состоянии системы. Некоторые предикаты должны иметь некоторое внутреннее состояние, например, предикат на основе счетчика, используемый для подкачки, но они по-прежнему не должны изменять никакое состояние в системе, к которой они применяются. С внутренним состоянием они также не могут быть разделены, однако они могут быть повторно использованы в своем потоке, если они поддерживают сброс между каждым последующим использованием.
Мелкозернистые интерфейсы: большая аудитория для ваших предикатов
В больших приложениях вы пишете очень похожие предикаты для совершенно разных типов, но у них есть общее свойство, например, отношение к клиенту . Например, на странице администрирования вы можете фильтровать журналы по клиенту ; на странице CRM вы хотите отфильтровать жалобы клиентов .
Для каждого такого типа X вам потребуется еще один CustomerXPredicate, чтобы отфильтровать его по клиенту. Но поскольку каждый X каким-то образом связан с клиентом, мы можем выделить его (Extract Interface in Eclipse) в интерфейс CustomerSpecific одним методом:
1
2
3
public interface CustomerSpecific {
   Customer getCustomer();
}
Этот детализированный интерфейс напоминает мне о чертах в некоторых языках, за исключением того, что он не имеет многоразовой реализации. Это также можно рассматривать как способ ввести ощущение динамической типизации в статически типизированных языках, поскольку он позволяет безразлично вызывать любой объект с помощью метода getCustomer (). Конечно, наш класс PurchaseOrder теперь реализует этот интерфейс.
Когда у нас есть этот интерфейс CustomerSpecific , мы можем определять предикаты для него, а не для каждого конкретного типа, как мы делали раньше. Это помогает использовать только несколько предикатов в большом проекте. В этом случае предикат CustomerPredicate совмещен с интерфейсом CustomerSpecific, на котором он работает, и имеет универсальный тип CustomerSpecific :
01
02
03
04
05
06
07
08
09
10
public final class CustomerPredicate implements Predicate<CustomerSpecific>, CustomerSpecific {
  private final Customer customer;
  // valued constructor omitted for clarity
  public Customer getCustomer() {
    return customer;
  }
  public boolean apply(CustomerSpecific specific) {
    return specific.getCustomer().equals(customer);
  }
}
Обратите внимание, что предикат может сам реализовывать интерфейс CustomerSpecific , следовательно, может даже оценивать себя!
При использовании подобных интерфейсов типа trait, вы должны позаботиться о обобщениях и немного изменить метод, который ожидает Predicate <PurchaseOrder> в классе PurchaseOrders , чтобы он также принимал любой предикат для супертипа PurchaseOrder:
1
2
3
public Iterable<PurchaseOrder> selectOrders(Predicate<? super PurchaseOrder> condition) {
    return Iterables.filter(orders, condition);
}
Спецификация в доменно-управляемом дизайне
Эрик Эванс и Мартин Фаулер вместе написали спецификацию шаблона , которая явно является предикатом. На самом деле слово «предикат» — это слово, используемое в логическом программировании, а спецификация шаблона была написана для объяснения того, как мы можем заимствовать некоторую мощь логического программирования в наших объектно-ориентированных языках.
В книге «Управляемый доменом дизайн» Эрик Эванс детализирует этот шаблон и приводит несколько примеров спецификаций, которые все выражают части домена. Точно так же, как эта книга описывает шаблон политики, который является ничем иным, как шаблоном стратегии применительно к домену, в некотором смысле шаблон спецификации можно рассматривать как версию предиката, посвященного аспектам домена, с дополнительным намерением четко обозначить и идентифицировать бизнес правила.
В качестве примечания, имя метода, предложенное в шаблоне спецификации: isSatisfiedBy (T): boolean , которое подчеркивает фокус на ограничениях домена. Как мы видели ранее с предикатами, атомы бизнес-логики, инкапсулированные в объекты Спецификации, могут быть рекомбинированы с использованием логической логики (или, а не любой, всех), как в шаблоне Интерпретатор .
В книге также описаны некоторые более продвинутые методы, такие как оптимизация при запросах к базе данных или хранилищу, а также использование ресурсов.  
Оптимизация при запросе
Ниже приведены приемы оптимизации, и я не уверен, что они вам когда-нибудь понадобятся. Но это правда, что предикаты довольно глупы, когда дело доходит до фильтрации наборов данных: они должны оцениваться только для каждого элемента в наборе, что может вызвать проблемы с производительностью для огромных наборов. Если хранение элементов в базе данных и заданный предикат, выбор каждого элемента только для того, чтобы отфильтровать их один за другим через предикат, не подходит для больших наборов.
Когда вы сталкиваетесь с проблемами производительности, вы запускаете профилировщик и находите узкие места. Теперь, если вызов предиката очень часто для фильтрации элементов из структуры данных является узким местом, то как вы это исправите?
Один из способов — избавиться от всего предиката и вернуться к жестко закодированному, более подверженному ошибкам, повторяющемуся и менее тестируемому коду. Я всегда сопротивляюсь этому подходу, пока я могу найти лучшие альтернативы для оптимизации предикатов, а таких много.
Во-первых, взгляните глубже на то, как используется код. В духе доменно-управляемого дизайна, поиск предметной области должен быть систематическим, когда возникает вопрос.
Очень часто существуют четкие схемы использования в системе. Хотя статистические, они предлагают большие возможности для оптимизации. Например, в нашем классе PurchaseOrders извлечение каждого ордера PENDING может использоваться гораздо чаще, чем любой другой случай, потому что именно так это имеет смысл с точки зрения бизнеса, в нашем воображаемом примере.
Друг Соучастие
На основе шаблона использования вы можете кодировать альтернативные реализации, которые специально оптимизированы для него. В нашем примере часто запрашиваемых отложенных ордеров мы кодируем альтернативную реализацию FastPurchaseOrder , которая использует некоторую предварительно вычисленную структуру данных для обеспечения готовности отложенных ордеров к быстрому доступу.
Теперь, чтобы извлечь выгоду из этой альтернативной реализации, у вас может возникнуть соблазн изменить его интерфейс для добавления специального метода, например, selectPendingOrders () . Помните, что раньше у вас был только общий метод selectOrders (Predicate) . Добавление дополнительного метода может быть приемлемым в некоторых случаях, но может вызвать некоторые проблемы: вы должны реализовать этот дополнительный метод и в любой другой реализации, и дополнительный метод может быть слишком специфичным для конкретного варианта использования, поэтому может не подходить для интерфейс.
Хитрость в использовании внутренней оптимизации с помощью точно такого же метода, который ожидает только предикаты, состоит в том, чтобы заставить реализацию распознавать предикат, с которым она связана. Я называю это « Friend Complicity» со ссылкой на ключевое слово Friend в C ++.
01
02
03
04
05
06
07
08
09
10
11
12
13
/** Optimization method: pre-computed list of pending orders */
private Iterable<PurchaseOrder> selectPendingOrders() {
  // ... optimized stuff...
}
 
public Iterable<PurchaseOrder> selectOrders(Predicate<? super PurchaseOrder> condition) {
  // internal complicity here: recognize friend class to enable optimization
  if (condition instanceof PendingOrderPredicate) {
     return selectPendingOrders();// faster way
  }
  // otherwise, back to the usual case
  return Iterables.filter(orders, condition);
}
Понятно, что это увеличивает связь между двумя классами реализации, которые в противном случае должны игнорировать друг друга. Кроме того, это только помогает с производительностью, если дан непосредственно предикат «друг», без декоратора или композита.
Что действительно важно с Friend Complicity, так это убедиться, что поведение метода никогда не скомпрометировано, контракт интерфейса должен выполняться всегда, с оптимизацией или без нее, просто может произойти улучшение производительности или нет. Также имейте в виду, что вы можете захотеть вернуться к неоптимизированной реализации однажды.  
SQL-скомпрометированы
Если заказы действительно хранятся в базе данных, то SQL можно использовать для их быстрого запроса. Кстати, вы, наверное, заметили, что само понятие предиката — это именно то, что вы ставите после предложения WHERE в запросе SQL.
Первый и простой способ по-прежнему использовать предикат, но при этом повысить производительность, — это для некоторых предикатов реализовать дополнительный интерфейс SqlAware с методом asSQL (): String, который возвращает точный SQL-запрос, соответствующий для оценки самого предиката. Когда предикат используется в хранилище, поддерживаемом базой данных, хранилище будет вызывать этот метод вместо обычного метода оценки (Predicate) или apply (Predicate) , а затем запрашивать базу данных с помощью возвращенного запроса.
Я называю такой подход SQL-компрометированным, поскольку предикат теперь загрязнен специфичными для базы данных деталями, которые он должен игнорировать чаще, чем нет.
Альтернативы использованию SQL напрямую включают использование хранимых процедур или именованных запросов : предикат должен предоставить имя запроса и все его параметры. Двойная диспетчеризация между репозиторием и переданным ему предикатом также является альтернативой: репозиторий вызывает предикат для своего дополнительного метода selectElements (this), который сам вызывает правильный метод предварительного выбора findByState (state): Collection в репозитории; Затем предикат применяет свою собственную фильтрацию к возвращенному набору и возвращает окончательный отфильтрованный набор.
категоризации
Подпотребление — это логическая концепция, чтобы выразить отношение одного понятия, которое охватывает другое, такое как «красный, зеленый и желтый относятся к термину цвет» ( Merriam-Webster ). Подразделение между предикатами может быть очень мощной концепцией для реализации в вашем коде.
Давайте рассмотрим пример приложения, которое транслирует котировки акций. При регистрации мы должны заявить, какие цитаты мы заинтересованы в наблюдении. Мы можем сделать это, просто передав предикат по акциям, который оценивается только для акций, в которых мы заинтересованы:
1
2
3
4
5
6
7
8
public final class StockPredicate implements Predicate<String> {
   private final Set<String> tickers;
   // Constructors omitted for clarity
 
   public boolean apply(String ticker) {
     return tickers.contains(ticker);
   }
 }
Теперь мы предполагаем, что приложение уже транслирует стандартные наборы популярных тикеров на темы сообщений, и у каждой темы есть свои предикаты; если бы он мог обнаружить, что предикат, который мы хотим использовать, «включен» или включен в один из стандартных предикатов, мы могли бы просто подписаться на него и сохранить вычисления. В нашем случае это достаточно простое добавление дополнительного метода к нашим предикатам:
1
2
3
public boolean encompasses(StockPredicate predicate) {
   return tickers.containsAll(predicate.tickers);
 }

Подчинение — это оценка другого предиката «сдерживания». Это легко, когда ваши предикаты основаны на наборах, как в примере, или когда они основаны на интервалах чисел или дат. В противном случае вам, возможно, придется прибегнуть к уловкам, подобным Friend Complicity, то есть к распознаванию другого предиката, чтобы решить, будет ли он включен или нет, в индивидуальном порядке.

В целом, помните, что в общем случае сложно реализовать подсчет, но даже частичное погружение может быть очень полезным, поэтому оно является важным инструментом в вашем наборе инструментов.
Вывод
Предикаты — это весело и могут улучшить как ваш код, так и ваш образ мышления!
Ура,
Единственный исходный файл для этой части доступен для скачивания cyriux_predicates_part2.zip (исправлена ​​неработающая ссылка)