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?
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.