Это четвёртая статья серии, посвящённая использованию класса Optional при обработке объектов с динамической структурой. В первой статье было рассказано о способах избежания NullPointerException в ситуациях, когда вы не можете или не хотите использовать Optional. Вторая статья посвящена описанию методов класса Optional в том виде, как он появился в Java 8. Третья — методам, добавленным в класс в Java 9.
Ну а в пятой статье я рассказываю о том, где внутри класса следует применять Optional, подвожу итоги и дарю каждому читателю, дочитавшему серию до конца, ценный подарок.
Класс о котором я хочу рассказать в этой статье возник при попытке найти решение реальной задачи. На абстрактном уровне постановка задачи звучит так: вы обращаетесь к некому сервису, который в случае успеха должен вернуть объект. Но запрос может закончится и неудачей. Причин для неудачи может быть несколько. И логика дальнейшей обработки ошибочной ситуации зависит от того, какова была причина неудачи.
Если сервис возвращает Optional, о причине мы ничего не узнаем. Значит надо использовать что-то похожее на Optional, но содержащее информацию об ошибке в случае неуспеха.
В примере с электрочайником, рассмотренном во второй статье этого цикла мы получали кипячёную воду, если на входе у нас есть и вода и электричество. При отсутствии воды или электричества мы получали Optional.empty() объект.
В случае электрочайника причину неудачи нетрудно определить. А вот если наш прибор приготавливает различные типы кофе, в случае неудачи было бы неплохо знать, в чем причина. Например, какого ингредиента недостаёт.
Для этого нам необходимо уметь возвращать не только результат обработки в случае успеха, но и информацию об обнаруженной проблеме в случае неудачи. Для наглядности я сравнивал в предыдущих статьях Optional футляром. Развивая эту аналогию, в данном случае нам необходим футляр с двойным дном.
К сожалению, Java позволяет использовать в качестве возвращаемого значения метода строго один объект. А нам, как мы видим, необходимо уметь возвращать либо один либо другой объект.
При всем моем уважении к создателям Java и понимая все сложности принятия решений о расширении языка новыми возможностями, я считаю это одним из недостатков языка, который можно было давно исправить.
Но пока это не сделано, я предлагаю использовать класс Result<SUCCESS, FAILURE>, разработка которого была инспирирована чтением одной из глав книги:
Functional Programming in Java. How functional techniques improve your Java programs. Pierre-Yves Saumont. Manning Pubn. ISBN 9781617292736Я очень советую прочитать вам эту книгу, если вы ее еще не прочитали. Итак, перейдем к рассмотрению класса.
Как его использовать внутри метода (в качестве возвращаемого значения)?
Класс позволяет запаковать в свои объекты (инстанции) сразу результат обработки в случае удачи либо информацию об ошибке в случае неудачи. Поэтому класс параметризуется двумя типами: Result<SUCCESS, FAILURE>.
Трюк состоит в том, что он позволяет запаковать либо одно, либо другое, но не оба вместе.
А дальше все просто: в случае успешной обработки мы записываем результат работы вашего метода как объект типа SUCCESS а в случае неудачи как объект типа FAILURE.
Как его использовать снаружи (в вызове метода)?
Для обработки полученного результата можно в зависимости от ситуации использовать простой либо более элегантный подход.
Простой подход двухшаговый. Сначала вы определяете, была ли обработка удачной. А затем, в зависимости от этого, разбираетесь отдельно либо с положительным результатом либо с ошибкой.
Элегантный подход заключается в использовании элементов функционального программирования из арсенала Java 8.
Но давайте перейдем к примерам.
Модифицируем электрочайник
Мы оттолкнемся от модели электрочайника из примера с электрочайником, рассмотренном во второй статье этого цикла.
Его исходные тексты а также исходные тексты рассмотренного в этой статье примера вы найдете в моём проекте на GitHub.
В примере мы пытались с использованием Java 8 Optional симулировать функционирование стационарного кипятильника, который можно встретить в офисах некоторых компаний.
Его схема представлена на картинке внизу:
Как можно видеть, для работы кипятильнику нужна вода и электроэнергия. На выходе кипятильник может выдавать сырую или кипяченую воду.
Краткое напоминание тем кто подзабыл или не читал вторую статью цикла
Сырую воду мы моделировали классом:
Как мы видим, мы можем получить из сырой воды кипячёную с помощью метода boil(). Это допущение вызвало справедливые нарекания читателей. Но оно сделано из соображений простоты кода.
Ну а класс моделирующий кипячёную воду совсем прост:
public class CupOfWater {
public CupOfBoiledWater boil() {
return new CupOfBoiledWater();}
}
Как мы видим, мы можем получить из сырой воды кипячёную с помощью метода boil(). Это допущение вызвало справедливые нарекания читателей. Но оно сделано из соображений простоты кода.
Ну а класс моделирующий кипячёную воду совсем прост:
public class CupOfBoiledWater {}
Вход модифицированного прибора можно описать таким вот интерфейсом:
public interface IBoilerInput2 {
void setAvailability(@Nullable CupOfWater water, boolean powerAvailable);
}
Не удивляйтесь числам-суффиксам в названии интерфейсов. В серии я рассматривал разные альтернативы реализации модели чайника.
Выход прибора мы определим вот так:
public interface IBoilerOutput3 {
Result<CupOfWater, String> getCupOfWater();
Result<CupOfBoiledWater, String> getCupOfBoiledWater();
}
В случае успеха мы получаем объект моделирующий сырую или кипяченную воду. А в случае неудачи – текст об ошибке. Разумеется вместо текста мы могли бы использовать более сложный объект с кодом ошибки, список ошибок, и т.д.
Поведение класса описывается интерфейсом:
interface IBoiler3 extends IBoilerInput2, IBoilerOutput3 {}
По сравнению со старым вариантом можно видеть, что вместо Optional<CupOfBoiledWater> мы используем теперь Result<CupOfBoiledWater, String>.
Чтобы рассмотренные ниже тестовые коды были понятнее, договоримся сначала о текстах ошибок, которые наш класс может выдавать:
...
public static final String WATER_NOT_AVAILABLE = "Water not available.";
public static final String POWER_NOT_AVAILABLE = "Power not available.";
public static final String BOTH_NOT_AVAILABLE = WATER_NOT_AVAILABLE + " " + POWER_NOT_AVAILABLE;
Пишем тест:
JUnit test Boiler3Test
public class Boiler3Test {
private IBoiler3 boiler;
@Before
public void setUp() throws Exception {
boiler = new Boiler3();
}
@Test
public void testBothNotAvailable() {
boiler.setAvailability(null, false);
assertFalse(boiler.getCupOfWater().isSuccess());
boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));
assertFalse(boiler.getCupOfBoiledWater().isSuccess());
boiler.getCupOfBoiledWater().ifFailure(message->assertEquals(message, Boiler3.BOTH_NOT_AVAILABLE));
}
@Test
public void testPowerAvailable() {
boiler.setAvailability(null, true);
assertFalse(boiler.getCupOfWater().isSuccess());
boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));
assertFalse(boiler.getCupOfBoiledWater().isSuccess());
boiler.getCupOfBoiledWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));
}
@Test
public void testWaterAvailable() {
boiler.setAvailability(new CupOfWater(), false);
assertTrue(boiler.getCupOfWater().isSuccess());
assertFalse(boiler.getCupOfBoiledWater().isSuccess());
boiler.getCupOfBoiledWater().ifFailure(message->assertEquals(message, Boiler3.POWER_NOT_AVAILABLE));
}
@Test
public void testBothAvailable() {
boiler.setAvailability(new CupOfWater(), true);
assertTrue(boiler.getCupOfWater().isSuccess());
assertTrue(boiler.getCupOfBoiledWater().isSuccess());
}
}
А вот и реализация класса электрочайника с помощью нашего нового класса Result<SUCCESS, FAILURE>:
public class Boiler3 implements IBoiler3 {
public static final String WATER_NOT_AVAILABLE = "Water not available.";
public static final String POWER_NOT_AVAILABLE = "Power not available.";
public static final String BOTH_NOT_AVAILABLE = WATER_NOT_AVAILABLE + " " + POWER_NOT_AVAILABLE;
@Nullable
private CupOfWater water;
private boolean powerAvailable;
@Override
public void setAvailability(@Nullable CupOfWater water, boolean powerAvailable) {
this.water = water;
this.powerAvailable = powerAvailable;
}
@Override
public Result<CupOfWater, String> getCupOfWater() {
return water == null
? Result.failure(WATER_NOT_AVAILABLE)
: Result.success(water);
}
@Override
public Result<CupOfBoiledWater, String> getCupOfBoiledWater() {
Result<CupOfWater, String> resultStep1 = getCupOfWater();
return resultStep1.isSuccess()
? powerAvailable
? Result.success(resultStep1.getSuccess().boil())
: Result.failure(POWER_NOT_AVAILABLE)
: powerAvailable
? Result.failure(WATER_NOT_AVAILABLE)
: Result.failure(BOTH_NOT_AVAILABLE);
}
}
Обратите внимание на задание значений в методе getCupOfWater() в зависимости от того, что должен вернуть метод.
Простой метод обработки результата продемонстрирован в третьей строчке метода getCupOfBoiledWater(). Вначале мы узнаем, каков результат с помощью resultStep1.isSuccess(). А затем в зависимости от ответа продолжаем обработку.
В тесте был продемонстрирован более функциональный способ обработки с помощью метода ifFailure:
boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));
Метод будет вызван только если результат обработки был ошибочным. При этом информация об ошибке (в данном случае это message) будет автоматически предоставлена вашему обработчику.
Как видите, все очень просто.
Ну а под конец – исходный текст самого класса Result:
Класс Result
public abstract class Result<SUCCESS, FAILURE> {
public abstract boolean isSuccess() ;
public abstract SUCCESS getSuccess();
public abstract FAILURE getFailure();
public abstract Result<SUCCESS, FAILURE> ifSuccess(Consumer consumerSuccess);
public abstract Result<SUCCESS, FAILURE> ifFailure(Consumer consumerFailure);
public static class Success<SUCCESS, FAILURE> extends Result<SUCCESS, FAILURE>{
private final SUCCESS _success;
private Success(SUCCESS success) {
_success = success;
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public SUCCESS getSuccess() {
return _success;
}
@Override
public FAILURE getFailure() {
throw new IllegalStateException("getFailure called on Success");
}
@Override
public String toString() {
return "Success [_success=" + _success + "]";
}
@Override
public Result<SUCCESS, FAILURE> ifSuccess(Consumer consumerSuccess) {
consumerSuccess.accept(_success);
return this;
}
@Override
public Result<SUCCESS, FAILURE> ifFailure(Consumer consumerFailure) {
return this;
}
}
public static class Failure<SUCCESS, FAILURE> extends Result<SUCCESS, FAILURE>{
private final FAILURE _failure;
private Failure(FAILURE failure) {
_failure = failure;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public SUCCESS getSuccess() {
throw new IllegalStateException("getSuccess called on Failure");
}
@Override
public FAILURE getFailure() {
return _failure;
}
@Override
public String toString() {
return "Failure [_failure=" + _failure + "]";
}
@Override
public Result<SUCCESS, FAILURE> ifSuccess(Consumer consumerSuccess) {
return this;
}
@Override
public Result<SUCCESS, FAILURE> ifFailure(Consumer consumerFailure) {
consumerFailure.accept(_failure);
return this;
}
}
public static <SUCCESS, FAILURE> Result<SUCCESS, FAILURE> failure(FAILURE failure){ return new Failure<>(failure);}
public static <SUCCESS, FAILURE> Result<SUCCESS, FAILURE> success(SUCCESS success){ return new Success<>(success);}
}
Пользуйтесь, код полностью свободен.
Как и в случае прошлых примеров, исходные тексты вы найдете проекте на GitHub.
В последней статье серии мы поговорим о некоторых тонкостях использования класса Optional.
Я планирую также вознаградить ваше терпение подарком. Каким? Об этом вы узнаете в следующей статье.
Иллюстрация: ThePixelman