Guide to Turning Mutable Use Cases into Immutable Ones

Guide to Turning Mutable Use Cases into Immutable Ones

About this article:

  • Language: Java

  • Framework: None

  • Difficulty: Intermediate

  • Related Topics: Immutability, concurrency, development practices


Problem Domain

Often, I encounter cases like this:

void mutableUseCase(Integer someInput) {
    /*
     * This method demonstrates a mutable use case where the variable `mutableVar`
     * depends on an internal process. The value of `mutableVar` is set only after
     * a condition is evaluated.
     */
    Integer mutableVar;
    /*
     * Internal process based on the input value.
     * This block could involve a complex computation or require external dependencies.
     */
    final Integer internalDependence = someHardProcess(someInput);
    /*
     * Condition where the value of `mutableVar` is determined.
     */
    if (someInput > 0) {
        mutableVar = internalDependence;
    } else {
        mutableVar = internalDependence + SOME_CALC;
    }

    nextStep(mutableVar);
}

void nextStep(final Integer someInput) {
    System.out.println(someInput);
}

Given an input, we need to define the value of mutableVar, which depends on a process (someHardProcess), such as a JDBC adapter call, and an additional condition like an if/else block, before passing it to a subsequent process (nextStep).

The hidden problem in such cases lies in reference variations in concurrent environments. While waiting for the value of the mutable variable to be resolved, another process could redefine the reference in memory.

Modern languages like Kotlin and Rust, among others, declare memory references as immutable by default.


Approaches to Solve This Problem

1. Using Java's Functional Approach with a Supplier

void approachUsingSupplierUseCase(Integer someInput) {
    final Integer internalProcess = someHardProcess(someInput);
    final Supplier<Integer> immutableInputSupplier = () -> {
        if (someInput > 0) {
            return internalProcess;
        } else {
            return internalProcess + SOME_CALC;
        }
    };

    nextStep(immutableInputSupplier.get());
}

This approach is my personal favorite because it leverages the closure property of functional programming, allowing access to all resources defined within the higher-order function, such as internalProcess, which is immutable.

() -> {
    if (someInput > 0) {
        return internalProcess;
    } else {
        return internalProcess + SOME_CALC;
    }
}

The execution of the immutable instance immutableInputSupplier ensures thread-safe execution within concurrent blocks, and its returned value is securely passed to the nextStep method.

Advantages:

  • With closures, it's possible to access all resources defined in the higher-order function.

  • Refactoring is more natural.

Disadvantages:

  • Testing the nested function is impossible without testing the entire container function.

2. Using a Stateless Class Method

Another way to address the initial problem is through class methods, such as getImmutableInput, serving the same purpose as the Supplier above.

⚠️ Important: This method must be a pure function, i.e., it should not depend on external state (commonly referred to as hidden state).

void approachUsingMethodReference(final Integer someInput) {
    final Integer internalProcess = someHardProcess(someInput);
    nextStep(getImmutableInput(someInput, internalProcess));
}

Integer getImmutableInput(final Integer input, final Integer internalDependence) {
    if (input > 0) {
        return internalDependence;
    } else {
        return internalDependence + SOME_CALC;
    }
}

Advantages:

  • Within Java, it avoids creating a new function object.

  • Since it's externally accessible, unit testing can be performed on the function independently.

  • As a stateless method, it can be declared static.

Disadvantages:

  • The method interface will have as many arguments as external dependencies required for its execution. In this example, there are only two: input and internalDependence.

  • It clutters the class contract with auxiliary methods.


Increasing Complexity

Rewriting the initial problem with an additional common scenario: a try/catch block. Depending on its execution, we will assign a specific value.

Here, the debated variable mutableVar is declared outside the try/catch block while awaiting the resolution of its value.

As seen in the block below, the possible outcomes for mutableVar can be:

  • internalDependence

  • internalDependence + SOME_CALC

  • 0

void mutableWithExceptionUseCase(final Integer someInput) {
    Integer mutableVar;
    try {
        final Integer internalDependence = someHardProcess(someInput);
        if (someInput > 0) {
            mutableVar = internalDependence;
        } else {
            mutableVar = internalDependence + SOME_CALC;
        }
    } catch (Exception e) {
        mutableVar = 0;
    }

    nextStep(mutableVar);
}

For this particular case, I developed a class called SafeSupplier, which complements Optional. It allows us to rewrite the above code as follows:

void approachUsingSafeSupplier(Integer someInput) {
    final Integer internalProcess = someHardProcess(someInput);
    nextStep(SafeSupplier.tryGet(() -> {
                if (someInput > 0) {
                    return internalProcess;
                } else {
                    return internalProcess + SOME_CALC;
                }
            })
            .ifErrorThenGet(() -> 0).get());
}

As shown, this refactoring results in a process that is not only safer in concurrent environments but also much more expressive and easier to understand.

SafeSupplier: A Reliable Way to Handle Errors in Functional Java Programming

The SafeSupplier class can simplify error handling in Java by providing a safe and expressive way to manage exceptions in functional-style code. With SafeSupplier, you can:

  • Safely attempt operations using tryGet, which wraps the result or captures any runtime exception.

  • Handle errors gracefully with methods like ifErrorThenGet, enabling fallback values in case of failures.

  • Throw custom exceptions when necessary using ifErrorThenThrow.

  • Access results safely with methods like getNullable, wrapped in Optional for enhanced safety and clarity.

public final class SafeSupplier<T> {
    private final T value;
    private final Throwable cause;

    private SafeSupplier(Throwable cause) {
        this.value = null;
        this.cause = cause;
    }

    private SafeSupplier(T value) {
        this.value = value;
        this.cause = null;
    }

    static public <T> SafeSupplier<T> tryGet(Supplier<T> supplier) {
        Preconditions.isNullOrEmpty(supplier, ()->
                new IllegalArgumentException("Supplier cannot be null"));
        try {
            return new SafeSupplier<>(supplier.get());
        } catch (RuntimeException exception) {
            return new SafeSupplier<>(exception);
        }
    }


    public boolean isError() {
        return cause != null;
    }

    public Optional<T> ifErrorThenGet(Supplier<T> supplier) {
        Preconditions.isNullOrEmpty(supplier, ()->
                new IllegalArgumentException("Supplier cannot be null"));
        if (isError()) {
            return Optional.ofNullable(supplier.get());
        }
        return Optional.ofNullable(value);
    }

    public SafeSupplier<T> ifErrorThenThrow(Function<Throwable, ? extends RuntimeException> exceptionSupplier) {
        Preconditions.isNullOrEmpty(exceptionSupplier, ()->
                new IllegalArgumentException("Exception supplier cannot be null"));
        if (isError()) {
            throw exceptionSupplier.apply(cause);
        }
        return this;
    }

    public Optional<T> getNullable() {
        if (isError()) {
            return Optional.empty();
        }
        return Optional.ofNullable(value);
    }
}

This lightweight utility ensures immutability and enhances code readability, making it a great addition for robust and concurrent-safe functional Java development.
Check the unit test section to learn how to implement and use SafeSupplier!

Resources

Conceptual use case: src/main/java/fitodev/article/MutableUseCase.java

SafeSupplier: src/main/java/fitodev/utils/SafeSupplier.java

SafeSupplier(Unit testing): src/test/java/fitodev/utils/SafeSupplierTest.java