Java 8 introduced with Optional
a functional datatype that enables the developer to work with optional values without nested if-statements. This can simplify your code a lot.
Consider this method to parse an argument.
Optional readFloatParameterUsingIfs( String parameterName, Map<String, Set<String>> parameters) { final Set values = parameters.get(parameterName); if (values == null) { return Optional.empty(); } if (values.size() != 1) { return Optional.empty(); } final String valueAsString = values.iterator().next(); final float valueAsFloat; try { valueAsFloat = Float.parseFloat(valueAsString); } catch (NumberFormatException e) { return Optional.empty(); } return Optional.of(valueAsFloat); }
The code works, it is even readable if you are used to an imperative programming style. But, when the code changes, with all the if-branches and return-statements, it is likely that errors will sneak in. So, you need a good test coverage.
With the new Optional
datatype in Java, you can code this method in a more functional way.
Optional readFloatParameterUsingOptionals( String parameterName, Map<String, Set<String>> parameters) { return Optional.ofNullable(parameters.get(parameterName)) .filter(values -> values.size() == 1) .map(values -> values.iterator().next()) .map(value -> { try { return Float.parseFloat(value); } catch (NumberFormatException e) { return null; }}); }
Instead of a sequence of if-statements the filter
and map
methods are combined in a fluent style, which encapsulate the handling of empty optionals. If a filter is applied to an optional and its value passes the test, the optional stays the same. If it does not or the optional is empty, it will be empty.
If a map
is applied to an empty optional it stays empty. If it has a value, the value is mapped to a new value. If the new value is null, the result will be also empty.
Now there is no structure of if-branches that allows errors to sneak in. Still, the parsing of string to a float with the embedded try-catch structure is still noisy. The Option
datatype of the framework Vavr provides a cleaner alternative.
Optional readFloatParameterUsingOption( String parameterName, Map<String, Set<String>> parameters) { return Option.of(parameters.get(parameterName)) .filter(values -> values.size() == 1) .map(values -> values.iterator().next()) .flatMap(Function1.lift(Float::parseFloat)) .toJavaOptional(); }
The api of Option
is pretty similar to Javas Optional
. The last method toJavaOptional()
converts the Vavr Option
to a standard Javas Optional
.
But, the clou is not the Option
class. It is the lift()
method in Function1
. This method wraps the exectution of another function or method. If it returns a null or raises an exception, an empty Option
value is returned. Otherwise the result value is put into the Option
value. The combination with flatMap
can now be used for an if-free concatenation of operations.
This approach works well if you have to handle optional values in your domain. In the example given above you might want to give the client of your method a hint why the parameter could not be resolved. In this case it is not a handling of an optional value but a validation and parsing of external data. Vavr offers a data type Validation
that supports this approach.
Validation readFloatParameterUsingValidation( String parameterName, Map<String, Set<String>> parameters) { return Option.of(parameters.get(parameterName)) .toValid("Parameter is not set") .flatMap(strings -> strings.size() == 1 ? Validation.valid(strings.iterator().next()) : Validation.invalid("Parameter is set several times")) .flatMap(s -> Try.of(() -> Float.parseFloat(s)) .toValid("The parameter is not a valid String")); }
A validation value is either valid and contains a value or is invalid and contains an error message. In our example we start with an Option
. With toValid
it is converted to a validation value. If the optional value is empty, it is transformed to an invalid value with the provided error message. Otherwise a valid value with the same value as the optional value is generated.
The first flatMap
checks if the parameter is unique. According to the check either a valid or an invalid message is created.
The second flatMap
converts the string to floating point number using the Try
datatype. This datatype receives a computation and executes it. If an exception is raised, the result is a Failure
containing the exception. Otherwise it is an Success
containing the result value.
The final toValid
method converts the Try
to a validation value.
Again, we have a non trivial validation logic just by combining function calls without any error-prone nested if-statements. If you like to play with the code, you can find it here: https://github.com/akquinet/NoMoreIfs