
This is the fourth article in the series on the use of the Optional class when processing objects with dynamic structure. The
first article was about ways to avoid NullPointerException in situations where you can’t or don’t want to use Optional.
The second article is devoted to the description of the methods of the class Optional as it appeared in Java 8.
The third is to the methods added to the class in Java 9.
Well, in the
fifth article I tell about where the Optional should be used inside the class, summarize and give every reader who has read the series to the end, a valuable gift.
The class about which I want to tell in this article arose while trying to find a solution to a real problem. At the abstract level, the formulation of the problem is as follows: you are accessing a certain service, which, if successful, must return the object. But the request may end in failure. There may be several reasons for failure. And the logic of further processing of the error situation depends on what was the cause of the failure.
If the service returns an Optional, we know nothing about the reason. So you need to use something similar to Optional, but containing information about the error in the event of failure.
In the example of the electric kettle, discussed in the
second article of this cycle, we received boiled water, if at the entrance we have both water and electricity. In the absence of water or electricity, we received an Optional.empty () object.
')
In the case of an electric kettle, it is not difficult to determine the cause of failure. But if our device prepares various types of coffee, in case of failure it would be nice to know the reason. For example, what ingredient is missing.
To do this, we need to be able to return not only the result of processing if successful, but also information about the detected problem in case of failure. For clarity, I compared in previous articles Optional case. Developing this analogy, in this case we need a double bottom case.
Unfortunately, Java allows you to use strictly one object as the return value of a method. And we, as we see, need to be able to return either one or another object.
With all due respect to the creators of Java and understanding all the difficulties of making decisions about extending the language with new features, I consider this one of the shortcomings of a language that could be fixed long ago.
But until this is done, I suggest using the Result <SUCCESS, FAILURE> class, the development of which was inspired by reading one of the chapters of the book:
Functional Programming in Java. How to improve your Java programs. Pierre-Yves Saumont. Manning Pubn. ISBN 9781617292736
I strongly advise you to read this book if you have not read it yet. So, we proceed to the consideration of the class.
How to use it inside the method (as a return value)?
The class allows you to pack into your objects (instances) immediately the result of processing in case of success, or information about an error in case of failure. Therefore, the class is parametrized by two types: Result <SUCCESS, FAILURE>.
The trick is that it allows you to pack either one or the other, but not both.
And then everything is simple: in case of successful processing, we write the result of your method as an object of type SUCCESS and in case of failure, as an object of type FAILURE.
How to use it outside (in the method call)?
Depending on the situation, a simple or more elegant approach can be used to process the result.
A simple two-step approach. First, you determine whether the processing was successful. And then, depending on this, you understand separately either with a positive result or with an error.
An elegant approach is to use functional programming elements from the Java 8 arsenal.
But let's move on to the examples.
Modify the electric kettle
We will push away from the model of the electric kettle from the example with the electric kettle, discussed in the
second article of this cycle.
Its source code as well as the source code of the example discussed in this article can be found in
my GitHub project .
In the example, we tried using Java 8 Optional to simulate the operation of a stationary boiler, which can be found in the offices of some companies.
His diagram is shown in the picture below:

As you can see, the boiler needs water and electricity to work. At the outlet, the boiler can produce raw or boiled water.
A brief reminder to those who forgot or did not read the second article of the cycleWe modeled raw water with a class:
public class CupOfWater { public CupOfBoiledWater boil() { return new CupOfBoiledWater();} }
As we can see, we can get boiled from raw water using the boil () method. This assumption has caused fair complaints from readers. But it is made for reasons of simplicity of the code.
Well, the class modeling boiled water is quite simple:
public class CupOfBoiledWater {}
The input of the modified device can be described with this interface:
public interface IBoilerInput2 { void setAvailability(@Nullable CupOfWater water, boolean powerAvailable); }
Do not be surprised to the numbers-suffixes in the names of the interfaces. In the series I considered different alternatives for the implementation of the kettle model.
The output of the device, we define like this:
public interface IBoilerOutput3 { Result<CupOfWater, String> getCupOfWater(); Result<CupOfBoiledWater, String> getCupOfBoiledWater(); }
If successful, we get an object modeling raw or boiled water. And in case of failure - the text of the error. Of course, instead of text, we could use a more complex object with an error code, a list of errors, etc.
The behavior of the class is described by the interface:
interface IBoiler3 extends IBoilerInput2, IBoilerOutput3 {}
Compared to the old version, we can see that instead of Optional <CupOfBoiledWater> we now use Result <CupOfBoiledWater, String>.
To make the test codes discussed below understand more clearly, we first agree on the error texts that our class can issue:
... 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;
We write the test:
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()); } }
And here is the implementation of the electric kettle class using our new Result <SUCCESS, FAILURE> class:
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); } }
Note the setting of values in the getCupOfWater () method depending on what the method should return.
A simple method for processing the result is shown in the third line of the getCupOfBoiledWater () method. First we will find out what the result is with resultStep1.isSuccess (). And then, depending on the answer, we continue processing.
The test demonstrated a more functional way of processing using the ifFailure method:
boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));
The method will be called only if the result of the processing was erroneous. In this case, information about the error (in this case, this message) will be automatically provided to your handler.
As you can see, everything is very simple.
Well, in the end - the source code of the Result class itself:
Result class 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);} }
Use, the code is completely free.
As in the case of past examples, you will find the source code of the
project on GitHub .
In the last article in the series, we’ll talk about some of the subtleties of using the Optional class.
I also plan to reward your patience with a gift. How? You will learn about this in the next article.
Illustration:
ThePixelman