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


Image source: PIRO4D pixabay.com

В этом посте я продолжу тему Optional in Java 8, начатую в

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

и продолженную в

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

Примеры кода для этих постов вы найдете на моей странице в GitHub:

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

Напомню, что мы рассматриваем объекты с динамической структурой и их реализацию с помощью Java 8 Optional. Под динамичностью структуры мы подразумеваем свойство объектов в зависимости от разных причин в каком-то смысле иметь или не иметь какие-то из своих частей. Очень часто отсутствие части объекта означает, что некоторые ожидаемые функции объект в момент запроса им не предоставляются. Так, в рассмотренном примере с офисным кипятильником таких функции две – выдать сырую воду или выдать кипяченную воду.

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

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

При этом мы смотрели на проблему в каком-то смысле со стороны Java. В этой категории моего блога, которая называется “Материализация Идей” я пытаюсь практически обосновать тезис, высказанный в посте Разработка Программ == Материализация Идей о том, что разработка software это не что иное как материализация идей. По крайней мере, я считаю, что такой взгляд на нашу профессию ускоряет и упрощает нашу работу и улучшает её результаты.

Что же даёт этот подход в случае с динамической структурой объектов? Давайте попытаемся посмотреть на проблему с другой стороны. А именно: рассмотрим типичные задачи, которые нам приходится решать при работе с такого рода объектами и то, как в их решении нам могут помочь Optional.

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

Умение абстрагировать задачи – одно из важнейших качеств архитекторов и разработчиков программных систем. Какие абстракции существуют и как они соотносятся друг с другом – споры на эту тему длятся десятилетия. Лично я придерживаюсь мнения, что иерархии абстракций объектов в программировании полезно вести на основе их семантики. Если так поступать, то на вершине нашей иерархии абстракций окажется всего несколько процессов. Среди них один из важнейших и наиболее употребимых является процесс, за которым исторически неудачно закрепилось название ETL … (Extract – Transform – Load). Другими словами, очень многое из того, чем занимаются наши системы, можно свести к последовательности трех операций:

  • Извлечение объекта из чего-то
  • Трансформация объекта(изменение его частей либо превращение в новый объект)
  • Помещение или запись объекта куда-то.

Java 8 использует для этой цели более современную терминологию, заменив extract на supply и load на consume. Соответственно и мы будем дальше в этом блоге использовать английский термин Supplier для обозначения создателя или передатчика данных на трансформацию и Consumer для потребителя или приёмника данных после трансформации. Таким образом вместо ETL патерна мы будем говорить о STC (Supplier – Transformer – Consumer) патерне.

Итак, что же мы имели в примере предыдущего блога, если на него посмотреть в рамках этой терминологии

Вначале был … Optional в Supplier

Рассмотренный в предыдущем посте офисный кипятильник является примером объекта типа Supplier. Суть его работы – выдавать кипяченную или сырую воду. Результаты его работы зависят от внешних условий, которые задавались с помощью булевых переменных. Но в большинстве практически интересных задач на вход подаются не простые переменные, а объекты. В том числе такие, которые могут принимать значение null.

Рассмотрим, как можно применить Optional, если поведение Supplier определяется не булевыми переменными, а “старорежимными” объектами, допускающими нулевые значения.

Футляр для старорежимного объекта

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

public interface IBoilerInput2 {
   
   void setAvailability(@Nullable CupOfWater water, boolean powerAvailable);
}

Нулевое значение объекта water означает, что вода в прибор из водопровода не поступает.
Тогда поведение прибора в целом задается следующим интерфейсом:

public interface IBoiler2 extends IBoilerInput2, IBoilerOutput {}

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

public class Boiler2Test {
   
   private IBoiler2 boiler;
   
   @Before
   public void setUp() throws Exception {
      boiler = new Boiler2();
   }

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

Если мы сравним эти тесты с тестами для кипятильника первой модели, то увидим их очень большое сходство. Проверка результатов у одноименных тестов из разных наборов одинакова. Ну а вход отличается тем, что вместо значения true для источника воды мы подаем объект, а вместо false -null.

А вот и сама реализация:

public class Boiler2 implements IBoiler2 {
   
   @Nullable
   private CupOfWater water;
   private boolean powerAvailable;
   

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

   @Override
   public Optional<CupOfWater> getCupOfWater() {
      return Optional.ofNullable(water);
   }

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

Как мы видим, метод Optional.ofNullable() позволяет элегантно “положить” в футляр опасный объект с потенциально нулевым значением. Если объект имеет нулевое значение, футляр будет пустой. В противном случае в нем лежит необходимый нам объект.

Рассмотрим теперь другую, не такую уж редкую ситуацию, когда некий ресурс представлен основным и резервным элементом.

Хорошо, когда есть заначка…

Заначка -это простонародное определение для резервного ресурса. Отвлечемся от эмоциональной стороны этого термина и рассмотрим техническую сторону вопроса.

В технических системах нередко ресурсы одного и того же вида могут быть доступны более чем одним способом.

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

Мы не будем усложнять задачу излишними деталями о неполной заполненности дождевого бака и его размере и просто используем снова знакомый уже класс CupOfWater.

Вход такого приспособления описывается таким образом:

public interface IWaterDispenserInput {
   
   void setAvailability(@Nullable CupOfWater firstPortion);
}

Если дождевая вода не собрана, то на входе мы имеем  нулевой объект, иначе – нормальный объект.

Выход прибора описывается следующим интерфейсом:

public interface IWaterDispenserOutput {
   
   CupOfWater getCupOfWater();
}

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

public interface IWaterDispenser extends IWaterDispenserInput, IWaterDispenserOutput {}

Как и в предыдущих примерах, подготовим вначале тесты для проверки поведения нашей реализации:

public class WaterDispenser1Test {
    private IWaterDispenser waterDispenser;

    @Before
    public void setUp() throws Exception {
        waterDispenser = new WaterDispenser1();
    }

    @Test
    public void testMainAvailable() {
        waterDispenser.setAvailability(new CupOfWater());
        assertNotNull(waterDispenser.getCupOfWater());
    }


    @Test
    public void testMainNotAvailable() {
        waterDispenser.setAvailability(null);
        assertNotNull(waterDispenser.getCupOfWater());
    }
}

Наши ожидания таковы: прибор выдает воду независимо от того, заполнен бак с дождевой водой или нет, поскольку в последнем случае вода возьмется из “резерва” (водопровода).
Рассмотрим теперь реализацию:

public class WaterDispenser1  implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElse(new CupOfWater());
    }
}

Как мы видим, в сцепку к методу ofNullable() добавился метод orElse. Если первый элемент выдаст пустой Optional (дождевой воды не накоплено) второй метод добавит от себя объект. Если же первый метод выдаст непустой Optional, второй метод просто пропустит его через себя и резерв останется нетронутым.
Эта реализация предполагала наличие резервного объекта. Если же объект перед этим необходимо создать (в нашем случае – накачать воду) можно использовать метод orElseGet() с параметром типа Supplier:

public class WaterDispenser2 implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElseGet(()->new CupOfWater());
    }
}

Не выпускаем джина из бутылки

В некоторых случаях ограничения на ваш API не позволяет использовать Optional в качестве возвращаемого значения.

Предположим, что наш интерфейс определен таким образом, что клиент всегда ожидает на выходе нашей функции некоторый объект. Если запрашиваемого ресурса на момент запроса нет, и мы не хотим возвращать null, нам остается одно средство – выбросить Exception. Тем самым мы не выпускаем джина из бутылки – не даем возможности выпущенному нулевому объекту обернутся уже в коде клиента NullPoiner Exception.

Может ли нам помочь в этом случае Java 8 Optional?  Да, может.

Но перед тем, как рассмотреть решение, подготовим тест, проверяющий корректность его работы:

@Test  (expected = IllegalStateException.class)
public void testMainNotAvailable() {
    waterDispenser.setAvailability(null);
    waterDispenser.getCupOfWater();
    fail("This code line must be not reached");
}

А вот и решение:

public class WaterDispenser3 implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElseThrow(()->new IllegalStateException("Resource not available"));
    }
}

Думаю, многих читателей это решение не очень убедит. В самом деле, чем это лучше проверки на null с помощью if?

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

Я же со своей стороны попытаюсь в одном из последующих постов переложить одно интересное (на мой взгляд) решение проблемы обработки Exception в цепочках функциональных вызовов.

В следующем посте этой серии я попытаюсь рассказать об использовании Java 8 Optional в оставшихся фазах STC процесса -при трансформации (Transform) и записи на хранение (Consume).

Кроме того, мы постараемся найти ответ на вопрос, является ли Optional частью функционального программирования.

Leave a Reply

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