Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Groovy: Is there a better way of handling @Immutable objects than copyWith method

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

like image 882
paliwodar Avatar asked Mar 11 '16 11:03

paliwodar


People also ask

When to use immutable objects?

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.

Should objects be immutable Java?

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.

What does it mean by saying immutable objects?

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.

What is immutable object in Java can you change the value?

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.


1 Answers

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)
}

High level explanation:

First of all it is required to notice that the code of: delivery.recipient.address.street = newStreet is interpreted as:

  1. Accessing recipient property of delivery object
  2. Accessing address of what was the result of the above
  3. Assigning property street 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.

Base case

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.

Recursive step

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.

Conclusion

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' 
}
like image 114
paliwodar Avatar answered Oct 18 '22 18:10

paliwodar