Search code examples
javaoopdesign-patterns

Updating deeply nested fields that are immutable


I have a heavily nested class structure that does not have setters.

@Value
@Builder(toBuilder = true)
public class ClassA {
    private String fieldA;
    private ClassB classB;
}

@Value
@Builder(toBuilder = true)
public class ClassB {
    private ClassC classC;
    private ClassD classD;
}

@Value
@Builder(toBuilder = true)
public class ClassC {
    private ClassE classE;
}


@Value
@Builder(toBuilder = true)
public class ClassD {
    private String fieldD;
}


@Value
@Builder(toBuilder = true)
public class ClassE {
    private String fieldE;
}

@Value comes from Lombok which sets all the fields to private finals. Since there are no setters and if I need to update fieldE, I need to update parent objects as well and it would like something like this:

classA = classA.toBuilder()
            .classB(classA.getClassB().toBuilder()
                .classD(classA.getClassB().getClassD().toBuilder()
                    .fieldD("abc")
                    .build())
                .build())
            .build();

Some of the fields that I need to update are about 8 levels deep and incase I need to check for nulls, the code can get quite messy.

I cannot modify the original POJOs and make them mutable. Is there a different approach I can do to make this easier to update fields? I thought of using the facade pattern, but I guess with that approach, I'll need to recreate the whole class hierarchy again. Any suggestions?


Solution

  • As mentioned by Mark Seemann, this problem can kind of be solved with lenses.

    The idea is that a Lens<A, B> represents a way to get a B from an instance of A, and a way to set a new B for an instance of A. Each field of your classes can then be represented by a lens.

    You can compose lenses. If you have a Lens<A, B>, and a Lens<B, C>, you can compose them to get Lens<A, C>. This is the key to setting a deeply nested field. You just need to compose lots of lenses together, then call set on that final composed lens.

    ClassA modifiedClassA = ClassA.CLASS_B_LENS
            .then(ClassB.CLASS_D_LENS) // I've called the composition operation "then"
            .then(ClassD.FIELD_D_LENS)
            .set(someClassA, "New Value!");
    

    Here is a possible implementation.

    interface Lens<Root, T> {
        T get(Root r);
        // here I generalised the set operation to a "modify",
        // where the previous value is also available
        Root modify(Root root, UnaryOperator<T> newValue);
    
        default Root set(Root root, T newValue) {
            return modify(root, x -> newValue);
        }
    
        default <U> Lens<Root, U> then(Lens<T, U> p2) {
            return new Lens<>() {
                @Override
                public U get(Root r) {
                    return p2.get(Lens.this.get(r));
                }
    
                @Override
                public Root modify(Root r, UnaryOperator<U> f) {
                    return Lens.this.modify(r, t -> p2.modify(Lens.this.get(r), f));
                }
            };
        }
    }
    

    You can then make a factory method like this to create lenses from a getter and a "wither". The latter can be generated by the lombok @With annotation. You can put all the null checks in this factory.

    static <Root, T> Lens<Root, T> make(Function<Root, T> getter, BiFunction<Root, T, Root> setter) {
        return new Lens<>() {
            @Override
            public T get(Root r) {
                if (r == null) return null;
                return getter.apply(r);
            }
    
            @Override
            public Root modify(Root root, UnaryOperator<T> f) {
                if (root == null) return null;
                return setter.apply(root, f.apply(get(root)));
            }
        };
    }
    

    You can then add some lenses to your classes as static fields. This part can be done by a code generator/annotation processor, if you know how to write one.

    @Value
    @With
    class ClassA {
        private String fieldA;
        private ClassB classB;
    
        public static final Lens<ClassA, String> FIELD_A_LENS = Lens.make(ClassA::getFieldA, ClassA::withFieldA);
        public static final Lens<ClassA, ClassB> CLASS_B_LENS = Lens.make(ClassA::getClassB, ClassA::withClassB);
    }
    
    @Value
    @With
    class ClassB {
        private ClassC classC;
        private ClassD classD;
    
        public static final Lens<ClassB, ClassC> CLASS_C_LENS = Lens.make(ClassB::getClassC, ClassB::withClassC);
        public static final Lens<ClassB, ClassD> CLASS_D_LENS = Lens.make(ClassB::getClassD, ClassB::withClassD);
    }
    
    // and so on...
    

    See also this medium article that takes a slightly different approach.