Java 8 Optional и обьекты с динамической структурой. Часть 2


Image source: PIRO4D pixabay.com

Этот второй пост в серии об Optional  в Java 8. Первый пост вы найдете здесь:

Java 8 Optional и объекты с динамической структурой. Часть 1

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

Перед тем как мы перейдем к рассмотрению конкретного примера использования, попытаемся ответить на вопрос, а что такое Optional<T>?

Я рискну дать собственное наглядное определение: в первом приближении Optional<T> это аналог футляра физического объекта, например – очков. Лежит ли объект внутри футляра, вы можете узнать с помощью метода isPresent(). Если он там лежит, вы можете взять его с помощью метода get().

Ну а чтобы подняться на следующий уровень понимания, читайте документацию 🙂

В этом посте  я попытался показать элегантность этого механизма на маленьком примере. Исходные тексты рассмотренного в этом посте примера вы найдете на GitHub:

https://github.com/vsirotin/Smartenesse-Java

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

Его схема представлена на картинке внизу:

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

Таким образом вход этого прибора можно описать таким вот интерфейсом:

public interface IBoilerInput {
   
   void setAvailability(boolean waterAvailable, boolean powerAvailable);
}

А выход вот таким:

public interface IBoilerOutput {
   
   Optional<CupOfWater> getCupOfWater();
   
   Optional<CupOfBoiledWater> getCupOfBoiledWater();

}

Здесь интересно отметить, что поскольку (в зависимости от входных данных) прибор может выдавать или не выдавать сырую и кипяченую воду, мы представляем результат вызова get… с помощью Optional<T>.
Поведение прибора в целом описывает интерфейс, объединяющий методы входа и выхода.

public interface IBoiler extends IBoilerInput, IBoilerOutput {}

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

public class CupOfWater {
   public CupOfBoiledWater boil() {return new CupOfBoiledWater();}
}

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

public class CupOfBoiledWater {}

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

public class Boiler1Test {
   
   private IBoiler boiler;
   
   @Before
   public void setUp() throws Exception {
      boiler = new Boiler1();
   }

   @Test
   public void testBothNotAvailable() {            
      boiler.setAvailability(false, false);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   
   @Test
   public void testPowerAvailable() {                
      boiler.setAvailability(false, true);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testWaterAvailable() {    
      boiler.setAvailability(true, false);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testBothAvailable() {     
      boiler.setAvailability(true, true);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertTrue(boiler.getCupOfBoiledWater().isPresent());
   }
}

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

  • С помощью пары has… get…
  • C помощью выброса Exception изнутри get при недоступности продукта
  • С помощью признака активности выдаваемого наружу продукта.

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

Посмотрите мое, наверняка не оптимальное решение:

public class Boiler1 implements IBoiler {
   
   private boolean waterAvailable;
   private boolean powerAvailable;
   

   @Override
   public void setAvailability(boolean waterAvailable, boolean powerAvailable) {
      this.waterAvailable = waterAvailable;
      this.powerAvailable = powerAvailable;
   }

   @Override
   public Optional<CupOfWater> getCupOfWater() {
      return waterAvailable ? Optional.of(new CupOfWater()) : Optional.empty();
   }

   @Override
   public Optional<CupOfBoiledWater> getCupOfBoiledWater() {
      if(!powerAvailable)return Optional.empty();
      return getCupOfWater().map(cupOfWater->cupOfWater.boil());
   }
}

Обратите внимание на последнюю значимую строчку листинга, где используется метод map()  из класса Optional. Таким образом вы можете строить цепочки обработки. Если на одном из звеньев цепочки выяснится, что дальнейшая обработка невозможна, вся цепочка вернет пустой ответ.

Ну вот пока и все на сегодня.

В следующем посте мы рассмотрим оставшиеся темы:

  • Использование Optional в специальных ситуациях обработки объектов (Persistence, Validation)
  • Так когда же его использовать?
  • Optional – это часть функционального программирования?

3 thoughts on “Java 8 Optional и обьекты с динамической структурой. Часть 2”

Leave a Reply

Your email address will not be published. Required fields are marked *