I am looking for a flexible way of "modifying" (copying with some values changed) immutable objects in groovy. There is a copyWith method but it allows you only to replace some properties of the object. It doesn't seem to be convenient enough.
Let's say we have a set of classes representing a domain design of some system:
@Immutable(copyWith = true)
class Delivery {
String id
Person recipient
List<Item> items
}
@Immutable(copyWith = true)
class Person {
String name
Address address
}
@Immutable(copyWith = true)
class Address {
String street
String postalCode
}
Let's assume I need to change street of delivery recipient. In case of regular mutable object it is just fine to perform:
delivery.recipient.address.street = newStreet
or (perhaps useful in some cases):
delivery.with {recipient.address.street = newStreet}
When it comes to do the same with immutable objects the best way according to my knowledge would be:
def recipient = delivery.recipient
def address = recipient.address
delivery.copyWith(recipient:
recipient.copyWith(address:
address.copyWith(street: newStreet)))
It is actually needed for Spock integration test code so readability and expressiveness matters. The version above cannot be used "on the fly" so in order to avoid creating tons of helper methods, I have implemented my own copyOn (since copyWith was taken) method for that which makes it possible to write:
def deliveryWithNewStreet = delivery.copyOn { it.recipient.address.street = newStreet }
I wonder however if there is an ultimate solution for that, present in groovy or provided by some external library. Thanks
Immutable objects can be useful in multi-threaded applications. Multiple threads can act on data represented by immutable objects without concern of the data being changed by other threads. Immutable objects are therefore considered more thread-safe than mutable objects.
Conclusion. Immutable objects don't change their internal state in time, they are thread-safe and side-effects free. Because of those properties, immutable objects are also especially useful when dealing with multi-thread environments.
The immutable objects are objects whose value can not be changed after initialization. We can not change anything once the object is created. For example, primitive objects such as int, long, float, double, all legacy classes, Wrapper class, String class, etc. In a nutshell, immutable means unmodified or unchangeable.
A mutable object can be changed after it's created, and an immutable object can't. That said, if you're defining your own class, you can make its objects immutable by making all fields final and private. Strings can be mutable or immutable depending on the language. Strings are immutable in Java.
For the sake of completeness I provide my implementation of copyOn method. It goes as follows:
class CopyingDelegate {
static <T> T copyOn(T source, Closure closure) {
def copyingProxy = new CopyingProxy(source)
closure.call(copyingProxy)
return (T) copyingProxy.result
}
}
class CopyingProxy {
private Object nextToCopy
private Object result
private Closure copyingClosure
private final Closure simplyCopy = { instance, property, value -> instance.copyWith(createMap(property, value)) }
private final def createMap = { property, value -> def map = [:]; map.put(property, value); map }
CopyingProxy(Object nextToCopy) {
this.nextToCopy = nextToCopy
copyingClosure = simplyCopy
}
def propertyMissing(String propertyName) {
def partialCopy = copyingClosure.curry(nextToCopy, propertyName)
copyingClosure = { object, property, value ->
partialCopy(object.copyWith(createMap(property, value)))
}
nextToCopy = nextToCopy.getProperties()[propertyName]
return this
}
void setProperty(String property, Object value) {
result = copyingClosure.call(nextToCopy, property, value)
reset()
}
private void reset() {
nextToCopy = result
copyingClosure = simplyCopy
}
}
It is then just a matter of adding the delegated method in Delivery class:
Delivery copyOn(Closure closure) {
CopyingDelegate.copyOn(this, closure)
}
First of all it is required to notice that the code of: delivery.recipient.address.street = newStreet
is interpreted as:
recipient
property of delivery
objectaddress
of what was the result of the abovestreet
with the value of newStreet
Of course the class CopyingProxy
does not have any of those properties, so propertyMissing
method will be involved.
So as you can see it is a chain of propertyMissing
method invocations terminated by running setProperty
.
In order to implement the desired functionality we maintain two fields: nextToCopy
(which is delivery at the beginning) and copyingClosure
(which is initialised as a simple copy using copyWith
method provided by @Immutable(copyWith = true)
transformation).
At this point if we had a simple code like delivery.copyOn { it.id = '123' }
then it would be evaluated as delivery.copyWith [id:'123']
according to simplyCopy
and setProperty
implementations.
Let's now see how would it work with one more level of copying: delivery.copyOn { it.recipient.name = 'newName' }
.
First of all we will set initial values of nextToCopy
and copyingClosure
while creating CopyingProxy
object same way as in the previous example.
Let's now analyse what would happen during first propertyMissing(String propertyName)
call. So we would capture current nextToCopy
(delivery object), copyingClosure
(simple copying based on copyWith) and propertyName
(recipient) in a curried function - partialCopy
.
Then this copying will be incorporated in a closure
{ object, property, value -> partialCopy(object.copyWith(createMap(property, value))) }
which becomes our new copyingClosure
. In the next step this copyingClojure
is invoked in the way described in Base Case part.
We have then executed: delivery.recipient.copyWith [name:'newName']
. And then the partialCopy
applied to the result of that giving us delivery.copyWith[recipient:delivery.recipient.copyWith(name:'newName')]
So it's basically a tree of copyWith
method invocations.
On top of that you can see some fiddling with result
field and reset
function. It was required to support more than one assignments in one closure:
delivery.copyOn {
it.recipient.address.street = newStreet
it.id = 'newId'
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With