Java 8 Optional и объекты с динамической структурой. Часть 4: Использование в преобразователях и потребителях

Dynamic structure
Image source: PIRO4D pixabay.com

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

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

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

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

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

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

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

Напомню, что мы рассматриваем объекты с динамической структурой и их реализацию с помощью Java 8 Optional<T>.

В предыдущем посте мы рассмотрели использование Optional<T> при разработке поставщиков объектов (Supplier).

В этом посте мы рассмотрим использование Optional в двух остававшихся звеньях патерна STC (Supply-Transform-Consume): в преобразователях (Transform) и потребителях (Consume).

Использование Optional в преобразователях (Transform)

Преобразователь (Transformer) получает на вход некий объект и либо его изменяет  либо преобразует в некий другой объект. В нашем случае, поскольку мы ограничиваемся использованием Optional<T>, в качестве объекта на входе мы имеем всегда Optional<T>. Напомним, что это можно себе представить как футляр или контейнер, в котором находится или не находится объект типа T.

Преобразовать его можно либо в “настоящий” объект какого-либо типа, либо в новый футляр с каким-либо новым объектом.

Мы не будем рассматривать тривиальный вариант преобразования Optional<T> снова в Optional<T>. Тогда на абстрактном уровне все оставшиеся варианты такого преобразования можно выразить в виде трех формул, приведенных внизу:

T t = f1(Optional<T> opt)

U u = f2(Optional<T> opt)

Optional<U> = f3(Optional<T> opt)

Кандидаты на роли функций преобразований f1, f2 и f3 – методы из класса Optional<T> представлены в этой таблице:

T U Optional<U>
Optional<T> filter()

map()

map() flatMap()

orElse()

orElseGet()

В предыдущих постах этого цикла мы уже рассмотрели большинство из этих методов. Нерассмотренными остались только filter и flatMap.

Ниже в этом посте мы рассмотрим примеры использования этих методов.

Фильтрование (использование метода filter)

В следующем примере мы рассмотрим использование метода filter() который возвращает объект только если футляр не пуст и содержащийся в нем объект удовлетворяет некоторому критерию.

Документация Oracle (https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) говорит нам об этом методе следующее:

Optional<T> filter(Predicate<? super T> predicate)
If a value is present, and the value matches the given predicate, return an Optional describing the value, otherwise return an empty Optional.

В нашем случае в качестве объекта мы будем использовать порцию воды в емкости для сбора дождевой воды. Не вдаваясь в разбор физических и химических особенностей, будем считать, что собранная вода может быть либо чистой (удовлетворять критерию) либо нет.

Максимально упрощенная схема прибора показана на рисунке внизу.

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

Полностью коды этого примера вы можете найти в упомянутом в начале поста проекте на GitHuB в package eu.sirotin.example.optional4

Вначале познакомимся с классом представляющим собранную дождевую воду:

public class RainWater {

   private final boolean clean;

   public RainWater(boolean clean) {
      this.clean = clean;
   }

   public boolean isClean() {
      return clean;
   }

}

Как вы можете видеть, с помощью метода isClean() можно узнать, является ли собранная вода чистой или нет.

Этот класс используется в качестве входного параметра в нашем приборе.

Этот же объект но в “футляре” используется на выходе прибора.

public interface IRainWaterDispenserInput { void setAvailability(@Nullable RainWater rainWater); }
public interface IRainWaterDispenserOutput {
   
   Optional<RainWater> getRainWater();
}

А полностью поведение прибора описывается составным интерфейсом:

public interface IRainWaterDispenser extends IRainWaterDispenserInput, IRainWaterDispenserOutput {}

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

public class RainWaterDispenser1Test {
    private IRainWaterDispenser rainWaterDispenser;

    @Before
    public void setUp() throws Exception {
        rainWaterDispenser = new RainWaterDispenser1();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        rainWaterDispenser.setAvailability(new RainWater(true));
        assertTrue(rainWaterDispenser.getRainWater().isPresent());
        assertTrue(rainWaterDispenser.getRainWater().get().isClean());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        rainWaterDispenser.setAvailability(null);
        assertFalse(rainWaterDispenser.getRainWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        rainWaterDispenser.setAvailability(new RainWater(false));
        assertFalse(rainWaterDispenser.getRainWater().isPresent());
    }
}

Ну а теперь приступим к рассмотрению реализации нашего класса с помощью Optional<T>.

Вот его полный текст:

public class RainWaterDispenser implements IRainWaterDispenser{
    @Nullable private RainWater rainWater;

    @Override
    public void setAvailability(@Nullable RainWater rainWater) {
        this.rainWater = rainWater;
    }

    @Override
    public Optional<RainWater> getRainWater() {
        return Optional.ofNullable(rainWater).filter(RainWater::isClean);
    }
    
}

Жирным шрифтом показано использование метода filter(). В качестве критерия используется значение возвращаемое методом объекта isClean().

Обратите внимание также на использование методов ofNullable() и filter() в цепочке вызовов. Неправда ли, выглядит очень элегантно?

Трансформация – (использование метода flatMap)

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

Его максимально упрощенная схема показана внизу.

А поведение прибора описывается вот такой семантической таблицей:

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

Полностью исходный текст примера вы найдете в package
eu.sirotin.example.optional5

Как всегда, начнем с интерфейсов описывающих вход и выход прибора:

public interface IRainWaterCleanerInput {
   
   void setAvailability(@Nullable RainWater rainWater);
}
public interface IRainWaterCleanerOutput {
   
   Optional<CupOfWater> getCleanedWater();
}

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

public class RainWaterCleanerTest {
    private IRainWaterCleaner rainWaterDispenser;

    @Before
    public void setUp() throws Exception {
        rainWaterDispenser = new RainWaterCleaner();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        rainWaterDispenser.setAvailability(new RainWater(true));
        assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        rainWaterDispenser.setAvailability(null);
        assertFalse(rainWaterDispenser.getCleanedWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        rainWaterDispenser.setAvailability(new RainWater(false));
        assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
    }
}

Ну а теперь рассмотрим и сам класс:

public class RainWaterCleaner implements IRainWaterCleaner {
    @Nullable private RainWater rainWater;

    @Override
    public void setAvailability(@Nullable RainWater rainWater) {
        this.rainWater = rainWater;
    }

    @Override
    public Optional<CupOfWater> getCleanedWater() {
        return Optional.ofNullable(rainWater).flatMap(w->Optional.of(new CupOfWater()));
    }    
}

Использование метода flatMap() выделено в коде жирным шрифтом. В отличие от метода map() этот метод возвращает не сам объект а футляр (контейнер), который может быть и пустой.

Использование Optional в потребителях объектов (Consume)

Использования класса Optional<T> в трансформаторах и потребителях различается незначительно, поскольку любой трансформатор это всегда “немножко потребитель”. Ведь любая трансформация начинается с “восприятия” входного объекта трансформатором. С моей личной точки зрения множество методов, которые можно использовать в обоих случаях можно описать формулой:

ConsumerMethods = TransformerMethods + ifPresent()

Документация на странице Oracle определяет семантику метода ifPresent() следующим образом:

void ifPresent(Consumer<? super T> consumer)
If a value is present, invoke the specified consumer with the value, otherwise do nothing.

Как мы видим, метод не возвращает какого-либо значения, но позволяет обработать объект в футляре, если он там присутствует. Если его там нет, ничего не происходит.

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

Схема прибора показана на рисунке внизу:

Полностью исходные тексты примера вы найдете в package

eu.sirotin.example.optional6

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

public class MixedWater extends CupOfWater {
   public MixedWater(CupOfWater water) {}
}

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

public interface IMixerOutput extends IRainWaterCleanerOutput {
   
   Optional<MixedWater> getMixedWater();
}

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

public interface IMixer extends IRainWaterCleanerInput, IMixerOutput {}

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

Составим тест для проверки корректности поведения нашего прибора:

public class MixerTest {
    private IMixer mixer;

    @Before
    public void setUp() throws Exception {
        mixer = new Mixer();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        mixer.setAvailability(new RainWater(true));
        assertTrue(mixer.getMixedWater().isPresent());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        mixer.setAvailability(null);
        assertFalse(mixer.getMixedWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        mixer.setAvailability(new RainWater(false));
        assertTrue(mixer.getMixedWater().isPresent());
    }
}

А вот и реализация основного класса:

public class Mixer extends RainWaterCleaner implements IMixer{
   
   private MixedWater result = null;

   @Override
   public Optional<MixedWater> getMixedWater() {
      super.getCleanedWater().ifPresent(this::mix);
      return Optional.ofNullable(result);

   }
   
   private void mix(CupOfWater water) {
      result = new MixedWater(water);
   }

}

Жирным шрифтом выделено использование метода ifPresent(). Как мы видим, в качестве входного параметра метода используется метод из нашего же класса mix(). Он в свою очередь ожидает в качестве входного параметра объект типа  CupOfWater. Заметьте, что футляр с объектом именно этого типа возвращает метод getCleanedWater().

Ну вот и все примеры, которые я хотел рассмотреть применительно к классу Optional<T>.

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