One useful pattern is to define a public interface which can report all of the properties associated with the class, as well as a package-private interface which extends it with a few additional members. A package-private version of the class should be mutable, but the public-facing one should be immutable. The public-facing class should hold a reference of the private interface type, and implement its members in terms of that private interface. Additionally, all classes in the package must maintain the invariant that no instance of the mutable type may ever be modified after a reference to it has been stored in a field which could be reachable on another thread.
Invoking a method like WithName(String name)
on the public face of the class could cause it to call WithName
on the encapsulated object, which would in most cases constructing a NameAdder
object which holds the new Name
value along with a reference to the encapsulated object itself. All methods other than GetName
and WithName
would chain to the encapsulated object; while GetName()
would return the new Name
, and WithName
would return a new NameAdder
object which holds the new Name
and a reference to the object encapsulated in the NameAdder
(rather than a reference to the NameAdder
itself).
An obvious problem with this approach is that more and more operands get performed upon an object, the chains of method invocations would become longer and longer. To mitigate that, the internal interface (and perhaps the public one as well) should include a Flatten
method. Calling Flatten
on an implementation of the interface should yield a new object which applies all of the changes that have been added to it via methods like WithName
. One way of accomplishing that would be to construct a new object where every field's value was computed by calling Get
on all of the objects involved. That could be expensive and inefficient, however, if it gets invoked upon an object that's very "deep".
An alternative approach would be to have the private interface include an AsFlattenedMutable
method which would an instance of the mutable class to which all appropriate changes had been applied, and to which no reference had ever been stored in any class field anywhere. The mutable class could implement AsFlattenedMutable
to return a clone of itself; a class like NameAdder
would implement AsFlattenedMutable
to invoke AsFlattenedMutable
upon the encapsulated object, modify the name stored in that mutable object, and return the object after the mutation. This would thus allow even a rather deep modification chain to be resolved rather quickly.
The only slight "gotcha" with that approach is if code modifies an object and then stores a reference into a non-final
field, the generated code could potentially resequence the operations so that the reference to the object gets stored before all the modifications are complete. To properly solve that may require having a new object store a reference to the mutable object into a final
field, and then copying the reference from that final-field into the non-final one. By my understanding of the JVM specification, storing a reference to an object into a final
field of an object under construction would prevent the deferral of any pending requests beyond the return of the constructor, and reading the value of that field would prevent the non-final field from being assigned until after the execution of the constructor [if code didn't read the reference from the new object, but instead simply used a pre-existing copy, then the call to the constructor--as well as any pending mutations to the object of interest--could be deferred until after the assignment].
oldBookObject copy (title = newTitle, year = newYear)
. F#:oldBookObject with { title = newTitle; year = newYear }