Image source: PIRO4D pixabay.com
В нескольких моих предыдущих заметках (новомодное слово «пост» мне употреблять не хочется) я писал о преимуществах Java 8 Optional<T> по сравнению с использованием null в качестве значения объекта.
Optional<T> позволяет нам элегантно и эффективно решить проблему, когда обрабатываемые объекты или их части могут в присутствовать либо отсутствовать.
В этой заметке мы сделаем шаг дальше и рассмотрим ситуации, когда наша обработка (например – вычисление, считывание или запись объекта на носитель и т.д.) может закончиться удачей или … неудачей.
В примере с электрочайником, рассмотренном в Java 8 Optional и объекты с динамической структурой. Часть 2 мы получали кипячёную воду, если на входе у нас есть и вода и электричество. При отсутствии воды или электричества мы получали Optional.empty() объект.
В случае электрочайника причину неудачи нетрудно определить. А вот если наш прибор приготавливает различные типы кофе, в случае неудачи было бы неплохо знать, в чем причина. Например, какого ингредиента недостаёт.
Для этого нам необходимо уметь возвращать не только результат обработки в случае успеха, но и информацию об обнаруженной проблеме в случае неудачи.
К сожалению, Java позволяет использовать в качестве возвращаемого значения метода строго один объект. А нам, как мы видим, необходимо уметь возвращать либо один либо другой объект.
При всем моем уважении к создателям Java и понимая все сложности принятия решений о расширении языка новыми возможностями, я считаю это одним из недостатков языка, который можно было давно исправить. Например так, как это сделано в Scala или Kotlin.
Но пока это не сделано, я предлагаю использовать класс Result<SUCCESS, FAILURE>, разработка которого была инспирирована чтением одной из глав книги:
Functional Programming in Java
How functional techniques improve your Java programs
Pierre-Yves Saumont
Manning Pubn
ISBN 9781617292736 472 pages
Я очень советую прочитать вам эту книгу, если вы ее еще не прочитали.
Итак, перейдем к рассмотрению класса.
Как его использовать внутри метода (в качестве возвращаемого значения)?
Класс позволяет запаковать в свои объекты (инстанции) сразу результат обработки в случае удачи и информацию об ошибке в случае неудачи. Поэтому класс параметризуется двумя типами: Result<SUCCESS, FAILURE>.
Трюк состоит в том, что он позволяет запаковать либо одно, либо другое, но не оба вместе.
А дальше все просто: в случае успешной обработки мы записываем результат работы вашего метода как объект типа SUCCESS а в случае неудачи как объект типа FAILURE.
Как его использовать снаружи (в вызове метода)?
Для обработки полученного результата можно в зависимости от ситуации использовать простой либо более элегантный подход.
Простой подход двухшаговый. Сначала вы определяете, была ли обработка удачной. А затем, в зависимости от этого, разбираетесь отдельно либо с положительным результатом либо с ошибкой.
Элегантный подход заключается в использовании элементов функционального программирования из арсенала Java 8.
Но давайте перейдем к примерам.
Модифицируем электрочайник
Мы оттолкнемся от модели электрочайника из примера
Java 8 Optional и объекты с динамической структурой. Часть 2
Его исходные тексты а также исходные тексты рассмотренного в этом посте примера вы найдете на GitHub:
https://github.com/vsirotin/Smartenesse-Java
В примере мы пытались с использованием Java 8 Optional симулировать функционирование стационарного кипятильника, который можно встретить в офисах некоторых компаний.
Его схема представлена на картинке внизу:
Как можно видеть, для работы кипятильнику нужна вода и электроэнергия. На выходе кипятильник может выдавать сырую или кипяченую воду.
Сырую воду мы будем моделировать классом:
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 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()); } }
А вот и реализация класса электрочайника:
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:
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);} }
Пользуйтесь, код полностью свободен.
Как и в случае прошлых примеров, исходные тексты вы найдете на:
https://github.com/vsirotin/Smartenesse-Java
В одной из следующих заметок я постараюсь рассказать, как класс Result<SUCCESS, FAILURE> позволит вам забыть о кошмаре Exceptions в Java. Это сделает ваш сон продолжительнее и глубже, а жизнь красочнее и многограннее 🙂