Search code examples
javareactive-programmingproject-reactorreactor

Project Reactor: Reactively transform mutable object's state with another Publisher in same sequence


I'm trying to learn reactive programming by refactoring some currently blocking code. Multiple times I have come across the problem of setting some mutable data object's state from within a Mono-sequence without subscribing to it. In the old code the object's fields' values were calculated by some blocking service which I now also do inside Monos.

So far I've usually been (ab)using flatMap to get the expected behaviour:

initExpensiveObject().flatMap(expObj -> initExpensiveField(expObj).map(expField -> {
    expObj.setExpensiveField(expField);
    return expObj;
})).subscribe(expObj -> System.out.println("expensiveField: " + expObj.getExpensiveField()));
import reactor.core.publisher.Mono;

public class Main {

    /**
     * Expensive, lazy object instantiation
     */
    public static Mono<ExpensiveObject> initExpensiveObject() {
        return Mono.fromCallable(ExpensiveObject::new);
    }

    /**
     * Expensive, async mapping (i.e. database access, network request):
     * ExpensiveObject -> int
     */
    public static Mono<Integer> initExpensiveField(ExpensiveObject expObj) {
        return Mono.just(1);
    }

    public static class ExpensiveObject {
        private int expensiveField = -1;

        public int getExpensiveField() {
            return expensiveField;
        }

        public void setExpensiveField(int expensiveField) {
            this.expensiveField = expensiveField;
        }
    }
}

While this flatMap-pattern works, I feel like there should be a more reactive solution. Considering there are so many operators in Mono alone, it intuitively feels wrong to "map" from one object to the same in order to mutate it's state. The "side-effect" operators (doOn*), however, don't allow to easily transform another publisher without subscribing to it.

I'm very much open to design improvements if there is no trivial solution to my problem because the code's design is still to sequential.


Solution

  • While this flatMap-pattern works, I feel like there should be a more reactive solution.

    It may not be the answer you want to hear, but the reactive solution is to ditch the mutability entirely. In more complex examples, passing mutable objects around in reactive chains can lead to unintentional side effects, which can cause some rather difficult to track down bugs. It's far easier to refactor the mutability away altogether.

    I'm very much open to design improvements if there is no trivial solution to my problem

    The "least changes" approach I would take would be to:

    • Make ExpensiveObject immutable. Remove the setter method, and provide another constructor that takes an explicit value for expensiveField.
    • Provide a "reactive" withExpensiveField() (or ofExpensiveField(), or something else entirely, take your pick!) method on ExpensiveObject that takes a Mono<Integer> for expensiveField and returns a Mono<ExpensiveObject>.
    • This then allows you to build your reactive chain with a single flatMap() call, and no mutable objects in sight:
      initExpensiveObject()
              .flatMap(expObj -> expObj.withExpensiveField(initExpensiveField(expObj)))
              .subscribe(expObj -> System.out.println("expensiveField: " + expObj.getExpensiveField()));
      

    Above code with modifications:

    public class Main {
    
        /**
         * Expensive, lazy object instantiation
         */
        public static Mono<ExpensiveObject> initExpensiveObject() {
            return Mono.fromCallable(ExpensiveObject::new);
        }
    
        /**
         * Expensive, async mapping (i.e. database access, network request):
         * ExpensiveObject -> int
         */
        public static Mono<Integer> initExpensiveField(ExpensiveObject expObj) {
            return Mono.just(1);
        }
    
        public static final class ExpensiveObject {
    
            private final int expensiveField;
    
            public ExpensiveObject() {
                expensiveField = -1;
            }
    
            private ExpensiveObject(int expensiveField) {
                this.expensiveField = expensiveField;
            }
    
            public int getExpensiveField() {
                return expensiveField;
            }
    
            public Mono<ExpensiveObject> withExpensiveField(Mono<Integer> expensiveField) {
                return expensiveField.map(ExpensiveObject::new);
            }
        }
    }
    

    You'll probably want to change the above depending on final design (with methods don't make much sense on a single field object for instance), but that puts across the main ideas.