Уберите проверку на null, используйте Optional

В течение многих лет защитное программирование казалось единственным выходом. Теперь, после многих лет борьбы с проверкой нуля, мы можем воспользоваться классом Optional, который был представлен в Java 8.

Я собираюсь показать вам простой тест класса Optional и то, что с ним можно сделать. Вы можете пойти гораздо дальше.

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

Вот основные классы:

public final class RentHistory {

  private Integer totalInThisMonth;

  private Integer totalFromTheBeginning;

  public RentHistory() {
  }

  public RentHistory(Integer totalInThisMonth, Integer totalFromTheBeginning) {
    this.totalInThisMonth = totalInThisMonth;
    this.totalFromTheBeginning = totalFromTheBeginning;
  }

  public RentHistory(Integer totalInThisMonth) {
    this.totalInThisMonth = totalInThisMonth;
  }

  public Integer getTotalInCurrentMonth() {
    return totalInThisMonth;
  }

  public Integer getTotalFromTheBeginning() {
    return totalFromTheBeginning;
  }

}
Вход в полноэкранный режим Выход из полноэкранного режима
public final class User {

  private RentHistory rentHistory;

  public User() {
  }

  public User(RentHistory rentHistory) {
    this.rentHistory = rentHistory;
  }

  public RentHistory getRentHistory() {
    return rentHistory;
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

public final class Test {

  public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println("Number of movies user has rented in this month is: " + getTotal(user.getRentHistory()));
  }

  private Integer getTotal(RentHistory history) {
    return history.getTotalInCurrentMonth();
  }

  public static void main(String[] args) {
    Test test = new Test();
    test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(4)));
  }

}
Вход в полноэкранный режим Выйти из полноэкранного режима

Пользователь получит следующее: Количество фильмов, взятых пользователем напрокат в этом месяце, составляет: 4

Теперь что произойдет, если мы создадим Rent history с помощью конструктора по умолчанию, то есть без количества фильмов, которые он посмотрел в текущем месяце:

public static void main(String[] args) {
    Test test = new Test();
    test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory()));
  }
Войти в полноэкранный режим Выход из полноэкранного режима

Мы получим очень красивый текст, который говорит: Количество фильмов, которые пользователь взял напрокат в этом месяце: null

Наверняка это не то, что мы хотим видеть в пользовательском интерфейсе. Что можно с этим сделать, может быть следующее:

public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println("Number of movies user has rented in this month is: " + getTotal(user.getRentHistory()));
  }

  private Integer getTotal(RentHistory history) {
    return history.getTotalInCurrentMonth() == null ? 0: history.getTotalInCurrentMonth();
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Это стандартный оборонительный подход, который мы использовали, чтобы избежать получения null — добавляем условие, проверяем на наличие null и выполняем действие. Теперь с помощью Optional мы можем сделать следующее:

public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    Optional<Integer> total = getTotal(user.getRentHistory());
    if (total.isPresent()) {
      System.out.println("Number of movies user has rented in this month is: " + total.get());
    } else {
      System.out.println("User has not rented any movie in the current month");
    }
  }

  private Optional<Integer> getTotal(RentHistory history) {
    return Optional.ofNullable(history.getTotalInCurrentMonth());
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Видите, что это дало нам…. да, ровно ничего! Вы никогда не должны менять использование обычного условия на использование isPresent! Это точно не тот способ, которым следует использовать Optional. Нужно было сделать вот что:

public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println(getTotal(user.getRentHistory())
        .map(total -> "Number of movies user has rented in this month is: " + total)
        .orElse("User has not rented any movie in the current month"));
  }

  private Optional<Integer> getTotal(RentHistory history) {
    return Optional.ofNullable(history.getTotalInCurrentMonth());
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Помните, когда вы имеете дело с нулями, Optional.map — это то, что вы должны попробовать.

Теперь давайте поговорим о другой ситуации. Что произойдет, если мы передадим пользователя, у которого вообще нет RentHistory, давайте проверим.

  public static void main(String[] args) {
    Test test = new Test();
    test.displayNumberOfMoviesRentInCurrentMonth(new User());
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Сейчас мы получаем следующее: Исключение в потоке «main» java.lang.NullPointerException at getTotal(Test.java:15)

Теперь мы можем сделать примерно следующее:

  public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println(getTotal(user.getRentHistory())
        .map(total -> "Number of movies user has rented in this month is: " + total)
        .orElse("User has not rented any movie in the current month"));
  }

  private Optional<Integer> getTotal(RentHistory history) {
    if (history == null){
      return Optional.empty();
    }
    return Optional.ofNullable(history.getTotalInCurrentMonth());
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

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

public final class User {

  private RentHistory rentHistory;

  public User() {
  }

  public User(RentHistory rentHistory) {
    this.rentHistory = rentHistory;
  }

  public Optional<RentHistory> getRentHistory() {
    return Optional.ofNullable(rentHistory);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

  public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println(getTotal(user.getRentHistory())
        .map(total -> "Number of movies user rent in this month is: " + total)
        .orElse("User has not rent any movie in the current month"));
  }

  private Optional<Integer> getTotal(Optional<RentHistory> history) {
    if (!history.isPresent()){
      return Optional.empty();
    }
    return Optional.ofNullable(history.get().getTotalInCurrentMonth());
  }
Войти в полноэкранный режим Выход из полноэкранного режима

Optional не предназначен для передачи в качестве аргумента! Это точно не тот путь, которым нужно идти. Мы собираемся провести рефакторинг! InteliJ уже говорит нам, что я могу внести изменения и ввести метод map, не стоит этого делать!

  public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println(getTotal(user.getRentHistory())
        .map(total -> "Number of movies user has rented in this month is: " + total)
        .orElse("User has not rented any movie in the current month"));
  }

  private Optional<Integer> getTotal(Optional<RentHistory> history) {
    return history.map(RentHistory::getTotalInCurrentMonth);
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Выглядит немного лучше, но мы все еще передаем Optional в качестве аргумента, а это плохо! Теперь мы собираемся изменить способ. Сначала мы будем использовать пользователя, а затем, если у пользователя есть его RentHistory, мы будем использовать ее в методе getTotal. Таким образом, теперь код будет выглядеть следующим образом:

public final class Test {

  public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println(user.getRentHistory()
        .map(this::getTotal)
        .map(total -> "Number of movies user has rented in this month is: " + total)
        .orElse("User has not rented any movie in the current month"));
  }

  private Optional<Integer> getTotal(RentHistory history) {
    return Optional.ofNullable(history.getTotalInCurrentMonth());
  }

  public static void main(String[] args) {
    Test test = new Test();
    test.displayNumberOfMoviesRentInCurrentMonth(new User());
  }

}
Вход в полноэкранный режим Выйти из полноэкранного режима

Приведенное выше решение почти идеально! Оно хорошо работает при неправильном вводе, но когда все в порядке, как сейчас…

  public static void main(String[] args) {
    Test test = new Test();
    test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(6)));
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

…он выдает ответ: _Количество фильмов, взятых пользователем напрокат в этом месяце, составляет: Необязательно[6]
_
Это так, потому что мы оборачиваем Optional с Optional. Что необходимо сделать, так это использовать flatMap:

public final class Test {

public void displayNumberOfMoviesRentInCurrentMonth(User user) {
    System.out.println(user.getRentHistory()
        .flatMap(this::getTotal)
        .map(total -> "Number of movies user has rented in this month is: " + total)
        .orElse("User has not rented any movie in the current month"));
  }

  private Optional<Integer> getTotal(RentHistory history) {
    return Optional.ofNullable(history.getTotalInCurrentMonth());
  }

  public static void main(String[] args) {
    Test test = new Test();
    test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(6)));
  }

}
Войти в полноэкранный режим Выход из полноэкранного режима

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

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

Вы можете представить, как это можно использовать для чего-то более сложного и комплексного, а не только для простого отображения текста.

Этот пост основан на одном из докладов, который провел господин Виктор Рентеа. Я очень рекомендую посмотреть на этого человека в действии!

Вот git-репозиторий для этого простого примера.

Оцените статью
devanswers.ru
Добавить комментарий