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 частью функционального программирования.