With immutable models, what would be the best way to batch several changes?
For example, let’s have a Book
that is immutable. I need to change both title
and year
. I could change one by one, but that would just create one additional object that is not going to be used. Is there any better way for this?
I can imagine only something with constructor:
new Book(oldBook, newTitle, newYear);
or to have external tool for cloning books, that we can use.
5
Depending on the number of fields, you might find it easier to use the Builder pattern to combine these changes than to create a separate constructor for each possible set of changes. It does requiring one object that is not going to be used (returned), but you can use one builder instance for as many books as you need to change.
public final class Book {
private final String title;
private final Integer year;
private final List<String> authors;
public static final class Builder {
private String title = null;
private Integer year = null;
private List<String> authors = null;
public Book build() {
return new Book(this);
}
public Builder from(Book other) {
withTitle(other.title);
withYear(other.year);
withAuthors(other.authors);
return this;
}
public Builder withTitle(String title) {
this.title = title;
return this;
}
public Builder withYear(Integer year) {
this.year = year;
return this;
}
public Builder withAuthors(List<String> authors) {
if (authors.isEmpty())
this.authors = Collections.emptyList();
else
this.authors = new ArrayList<String>(authors);
return this;
}
}
private Book(Builder builder) {
this.title = builder.title;
this.year = builder.year;
this.authors = builder.authors;
}
public String getTitle() {
return title;
}
public Integer getYear() {
return year;
}
public List<String> getAuthors() {
return Collections.unmodifiableList(authors);
}
}
To make a new object from an old one:
Book oldBook = new Book.Builder()//
.withTitle("Hello")//
.withYear(1900)//
.withAuthors(Arrays.asList("Alice"))//
.build();
Book newBook = new Book.Builder()//
.from(oldBook)//
.withTitle("World")//
.withYear(1901)//
.build();
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].
Here’s a twist on Matt’s answer, using the builder pattern combined with Java 8’s new functional capabilities:
Book.java
import java.util.*;
import java.util.function.Consumer;
public class Book
{
private String title;
private List<String> authors;
private int year;
public Book(String title, List<String> authors, int year) {
this.title = title;
this.authors = new ArrayList<>(authors);
this.year = year;
}
/*
* Start with the current Book, create a BookBuilder with all the same properties, then send that through a functional lambda for changing what needs changed.
*/
public Book cloneWith(Consumer<BookBuilder> bookRebuilder) {
BookBuilder bookBuilder = new BookBuilder(getTitle(), getAuthors(), getYear());
bookRebuilder.accept(bookBuilder);
return new Book(bookBuilder.getTitle(), bookBuilder.getAuthors(), bookBuilder.getYear());
}
public String getTitle() {
return title;
}
public int getYear() {
return year;
}
public List<String> getAuthors() {
return new ArrayList<String>(authors);
}
public String toString() {
StringBuilder str = new StringBuilder();
str.append(title);
str.append(", by ");
for(String author : authors) {
str.append(author);
str.append(", ");
}
str.append("published ");
str.append(year);
return str.toString();
}
}
BookBuilder.java (this could just as easily be a static internal class)
import java.util.*;
public class BookBuilder
{
private String title;
private List<String> authors;
private int year;
public BookBuilder(String title, List<String> authors, int year) {
this.title = title;
this.authors = authors;
this.year = year;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<String> getAuthors() {
return authors;
}
public void setAuthors(List<String> authors) {
this.authors = authors;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
}
Main.java
import java.util.*;
public class Main
{
public static void main(String[] args) {
Book book1 = new Book("Pride and Prejudice", Arrays.asList("Jane Austen"), 1813);
Book book2 = book1.cloneWith((baseBook)-> {
baseBook.setTitle("Pride and Prejudice and Zombies");
baseBook.setYear(2009);
List<String> authors = baseBook.getAuthors();
authors.add("Seth Grahame-Smith");
baseBook.setAuthors(authors);
});
System.out.println(book1.toString());
System.out.println(book2.toString());
}
}
One big difference is that, instead of having to use a chain of fluent calls that might be hard to break, we can interject other logic within the lambda closure. Here BookBuilder
could also be made fluent so it can be just as terse.
3