Статьи

Лямбды для свободно и стабильно API

Несколько недель назад я написал вступление о лямбдах на Java 8 . Во введении я объяснил, что такое лямбда и как использовать их в сочетании с новым API-интерфейсом Stream, который также представлен в Java 8.

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

Lambdas предлагает вам возможность создавать более гибкие API. Чтобы показать это, в качестве примера я хотел бы использовать UserStore который облегчает UserStore и сохранение пользователей с использованием базы данных. Его общедоступный API обычно выглядит следующим образом.

1
2
3
4
5
6
public interface UserStore {
  User find(Long id);
  List<User> findByLastname(String lastname);
  List<User> findByCompany(String company);
  ..
}

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

Некоторые платформы предлагают обходные пути для этого беспорядка. Если вы использовали Hibernate, вы, вероятно, знаете, что они предоставляют findByExample с помощью findByExample где вы предоставляете User в качестве примера объекта, который предоставляет свойства и значения для запроса. Любое значение, установленное в этом примере объекта, используется для запроса, в то время как любое поле с null исключается из запроса. Вы можете немного подправить это поведение, но есть ряд проблем с этим подходом. Подумайте о значениях по умолчанию, обязательных полях (то есть полях, которые нельзя сделать
null ) и неизменность. iBatis, MyBatis, а также Spring Data используют генерацию кода, чтобы сэкономить ваше время на реализацию всех этих методов, оставляя API раздутым со списком методов findBy .

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

Лямбды могут помочь нам отделить часть запроса от спецификации фильтра. Давайте изменим функции findBy на одну функцию, которая принимает лямбду.

1
2
3
4
public interface UserStore {
  User find(Long id);
  List<User> findBy(Predicate<User> p);
}

Это намного лучший API. Очевидно, это немного наивно, так как предикат проверяет объект User ; Вы обычно хотите фильтровать, используя ваш запрос к базе данных. Тем не менее, он достаточно хорошо подходит для целей этого примера, и вы можете поэкспериментировать со своими собственными лямбдами для фильтрации с использованием запросов к базе данных. [Примечание: Predicate поставляется с Java 8 и находится в пакете java.util.function .]

Прежде чем вы захотите рассердиться, потому что по крайней мере с предыдущим API предикаты были объединены в одном месте, мы все еще можем связывать (общие) предикаты. Например, создавая служебный класс UserPredicates который их содержит.

1
2
3
4
5
public final class UserPredicates {
  public static Predicate<User> lastname(String matcher) {
    return candidate -> matcher.equals(candidate.getLastname());
  }
}

Использование нового API UserStore стало довольно простым.

1
2
3
static import UserPredicates.lastname;
 
userStore.findBy(lastname("<lastname>");

В UserStore осталась одна вещь, которая меня действительно беспокоит. Функция find(id) возвращает пользователя. Но что, если такого пользователя нет?

По желанию

Чтобы улучшить это, мы можем (и должны) взглянуть на еще одну новую функцию Java 8 — «Необязательно». Это своего рода реализация Java монады. Это очень похоже на Option Скала.

С помощью Optional мы можем лучше выразить, что функция может возвращать значение, но не обязательно, и предотвращать использование null . В нашем примере find(id) возвращающем Optional четко указано, что мы можем найти пользователя с запрошенным идентификатором, но, возможно, такого пользователя не существует.

1
2
3
4
public interface UserStore {
  Optional<User> find(Long id);
  List<User> findBy(Predicate p);
}

Теперь API не только документирует тот факт, что вы можете получить пользователя, но и никогда не возвращает null . Я считаю API, который никогда не возвращает null намного безопаснее. Однажды новый программист может не осознавать, что find может вернуть null и результатом является исключение нулевого указателя. Надеюсь, команда поймает это перед началом производства. Исключения с null указателем действительно легко предотвратить, если вы никогда не используете null .

Мы можем использовать функции в Optional для получения значения от пользователя, если оно доступно, или для получения значения по умолчанию в противном случае. Например, чтобы безопасно получить фамилию пользователя, мы пишем следующее.

1
2
Optional<User> user = userStore.find(id);
String lastname = user.map(User::getLastname).orElse("");

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

Что если нам нужно отправить пользователю электронное письмо с восстановлением пароля, если оно найдено?

1
2
Optional<User> user = userStore.find(id);
user.ifPresent(passwordReset::send);

Сброс пароля отправляется, если пользователь найден, иначе ничего не происходит.

Поскольку Java не поддерживает деструктуризацию, как и многие другие языки, которые могут быть (например, Haskell, Clojure и Scala), мы ограничены функциями Optional . Это делает Optional более слабым, чем его эквивалент в любом из этих языков.

строитель

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

1
2
3
4
public interface Mailer {
  void sendTextMessage(TextMessageBuilder message);
  void sendMimeMessage(MimeMessageBuilder message);
}

Чтобы использовать Mailer нам нужно передать в нее конкретного строителя. Эти компоновщики имеют общий интерфейс, но различаются по типу создаваемых ими сообщений. А у Mailer есть отдельные методы, потому что он должен добавлять различную информацию в зависимости от используемого типа. Из-за этого любой клиентский код тесно связан для передачи правильного компоновщика.

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

1
2
3
4
5
6
7
8
9
public interface Mailer {
  void sendTextMessage(MessageConfigurator configurator);
  void sendMimeMessage(MessageConfigurator configurator);
 
  @FunctionalInterface
  interface MessageConfigurator {
    MessageBuilder configure(MessageBuilder message);
  }
}

Чтобы использовать Mailer все, что нам нужно сделать, это предоставить лямбду для построения сообщения.

1
2
3
4
5
mailer.sendTextMessage(message ->
  message.from(sender).to(recipients)
      .subject("APIs")
      .body("Lambdas can make for more fluent and stable APIs")
);

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

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

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

Ссылка: Lambdas для плавных и стабильных API от нашего партнера по JCG Барта Баккера в блоге Software Craft .