Here is a design pattern I've found useful for "mutating" immutable objects in non-functional languages, such as Java, which don't have good support for it (unlike for example Scala's copy methods). I shall call it the Phase Change Pattern, because of how it freezes and melts objects to make them immutable and back again mutable, the same way as water can be frozen and melted.
This pattern consist of the following parts:
- An immutable class with some properties
- A mutable class with the same properties
- The mutable class has a freeze() method for converting it to the immutable class
- The immutable class has a melt() method for converting it to the mutable class
- Both of the classes have package-private copy constructors that take the other class' instance as parameter, copying all fields (making immutable/mutable copies of the field values when necessary)
The freeze
method plays a similar role as the build
method in the Builder pattern, but it's named after Ruby's freeze
method. The name melt
was chosen as a metaphor based on the thermodynamic phase changes. I think it has better connotations than the other alternatives I considered: "build - destroy", "save - edit", "persist - dispersist" (or can "transient" be made a verb?)
Code Example
The following code is taken from the Jumi Test Runner project where I originally invented this pattern.
Usage
The classes can be used like this to create nice immutable classes that may be freely passed around:
SuiteConfiguration config = new SuiteConfigurationBuilder() .addToClasspath(Paths.get("something.jar")) .addJvmOptions("-ea") .freeze();
But the pattern also makes it possible, in a method that takes the immutable object as parameter, to augment it with new values:
config = config.melt() .addJvmOptions("-javaagent:extra-agent.jar") .freeze();
This is useful in situations where the code that creates the original immutable object does not know all the arguments, but some of the arguments are known only much later by some other code.
Immutable Class
Here is the immutable class. Its default constructor sets all fields to their default values. The copy constructor will need to make immutable copies of all mutable properties (e.g. java.util.List
). The copy constructor takes the builder as parameter, making it easier to match the field names than if each property had its own constuctor parameter. The cyclic dependency is a small price to pay for this convenience. There are getters for all properties. Also this class can be made a value object by overriding equals
, hashCode
and toString
.
@Immutable public class SuiteConfiguration { public static final SuiteConfiguration DEFAULTS = new SuiteConfiguration(); private final List<URI> classpath; private final List<String> jvmOptions; private final URI workingDirectory; private final String includedTestsPattern; private final String excludedTestsPattern; public SuiteConfiguration() { classpath = Collections.emptyList(); jvmOptions = Collections.emptyList(); workingDirectory = Paths.get(".").normalize().toUri(); includedTestsPattern = "glob:**Test.class"; excludedTestsPattern = "glob:**$*.class"; } SuiteConfiguration(SuiteConfigurationBuilder src) { classpath = Immutables.list(src.getClasspath()); jvmOptions = Immutables.list(src.getJvmOptions()); workingDirectory = src.getWorkingDirectory(); includedTestsPattern = src.getIncludedTestsPattern(); excludedTestsPattern = src.getExcludedTestsPattern(); } public SuiteConfigurationBuilder melt() { return new SuiteConfigurationBuilder(this); } @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } // getters public List<URI> getClasspath() { return classpath; } public List<String> getJvmOptions() { return jvmOptions; } public URI getWorkingDirectory() { return workingDirectory; } public String getIncludedTestsPattern() { return includedTestsPattern; } public String getExcludedTestsPattern() { return excludedTestsPattern; } }
Mutable Class
Here is the mutable class. Its constructors and freeze
method are a dual to the immutable class' constructors and melt
method. The defaults are written only once, in the immutable class. This class has both getters and setters for all the properties. All the mutator methods return this
to enable method chaining.
@NotThreadSafe public class SuiteConfigurationBuilder { private final List<URI> classpath; private final List<String> jvmOptions; private URI workingDirectory; private String includedTestsPattern; private String excludedTestsPattern; public SuiteConfigurationBuilder() { this(SuiteConfiguration.DEFAULTS); } SuiteConfigurationBuilder(SuiteConfiguration src) { classpath = new ArrayList<>(src.getClasspath()); jvmOptions = new ArrayList<>(src.getJvmOptions()); workingDirectory = src.getWorkingDirectory(); includedTestsPattern = src.getIncludedTestsPattern(); excludedTestsPattern = src.getExcludedTestsPattern(); } public SuiteConfiguration freeze() { return new SuiteConfiguration(this); } // getters and setters public List<URI> getClasspath() { return classpath; } public SuiteConfigurationBuilder setClasspath(URI... files) { classpath.clear(); for (URI file : files) { addToClasspath(file); } return this; } public SuiteConfigurationBuilder addToClasspath(URI file) { classpath.add(file); return this; } ... }
Most of the mutators have been omitted for brevity. The full source code of these classes is in Github.
I would suggest you to take a look at Lombok's builder again which has a toBuilder functionality (added recently) which does this exactly. Check out 1.16.6 or later versions
ReplyDelete