Question

I will use example as its easier to show what I want to do. I have three classes X, Y and Z, and I would like to be able to have them as immutable objects. Here they are.

Class X :

public class X{
    private int id;
    private String dataX;

    private Collection<Y> collectionOfY;
    private Collection<Z> collectionOfZ;

    /* Constructors */
    /* Getters */
    /* "with" functions */

}

Class Y :

public class Y{

    private int id;
    private String dataY;

    private Collection<X> collectionOfX;
    private Collection<Z> collectionOfZ;

    /* Constructors */
    /* Getters */
    /* "with" functions */

}

Class Z :

public class Z{

    private int id;
    private String dataZ;

    private Collection<X> collectionOfX;
    private Collection<Y> collectionOfY;

    /* Constructors */
    /* Getters */
    /* "with" functions */

}

So they are linked with each other and form a graph with a cyclic structure. I would like to have immutable object if possible. I've implemented "With" functions which will send back a copy of a given object with just one property changed.

But, if I implement a total immutability, I will possibly cause a replication of every data linked (directly or indirectly) to the object I changed : Changing "dataZ" usually means that I want to replace the old object by the newly created one in linked X and Y. Which will be replicated due to the change, etc ...

Another solution would be to render Collections which link the three classes not immutable (and only have a partial immutability) but then I lost almost all interest of trying to have immutability.

Anyone have a good idea ? Or am I asking for the impossible ? Should I return to the old setter (even if it means more complex concurrency management) ?

Thanks in advance ;)

Was it helpful?

Solution

C# is able to get around this with its built-in Lazy<T>. A Lazy is an object whose actual value has not yet been computed - when you construct a Lazy, you pass a factory function as a parameter.

You can ask a Lazy for its value with the Value property. If the factory function has already run, then the already-constructed value is returned. If the factory function hasn't run, then it will run at this time. Whatever the case, the return of the Value property is always the same. Once a particular Lazy's value has been determined, it sticks.

A lazy is an immutable reference to a possibly-not-yet constructed object. It uses mutability internally, but from the outside, it appears to be immutable.

In your case, if you had a Java equivalent to Lazy, you would change your collections from, say, Collection<Y> to Collection<Lazy<Y>>. This would enable an instance of X to refer to some not-yet-constructed instances of Y. And the code which constructs the Xs, Ys, and Zs would not directly build those instances, but would instead build instances of Lazy. These instances would take factory functions as parameters; these factory functions, in turn, would need to reference to some of the Lazy values. This means that, within the context of the function that's constructing and wiring these things together, you'll need to have mutable references to Lazy instances.

To see what I mean, if you try to create a cycle of two objects (I'm not completely up on Java 8, so I might have syntax errors):

Lazy<X> a;
Lazy<Y> b;

a = new Lazy<X>(() -> {
    List<Y> ys = new ArrayList<Y>();
    ys.add(b.getValue());
    return new X(ys);
});

b = new Lazy<Y>(() -> {
    List<X> xs = new ArrayList<X>();
    xs.add(a.getValue());
    return new Y(xs);
});

In practice, I don't think that will work. I think closed-over variables need to be final in Java (this isn't the case in C#). So I think you'd need to actually do this:

final Lazy<X>[] a = new Lazy<X>[1];
final Lazy<Y>[] b = new Lazy<Y>[1];

a[0] = new Lazy<X>(() -> {
    List<Y> ys = new ArrayList<Y>();
    ys.add(b[0].getValue());
    return new X(ys);
});

b[0] = new Lazy<Y>(() -> {
    List<X> xs = new ArrayList<X>();
    xs.add(a[0].getValue());
    return new Y(xs);
});

This works because the two lambdas aren't evaluated immediately. As long as a[0] and b[0] get set to valid values before these lambdas are executed, everything will work fine.

Note that this uses mutability, but mutability in very limited scopes. There's mutability inside the Lazy instances, but these instances appear to be immutable. There's mutability in the wire-up function, but that function will run up front and terminate, whereas the constructed objects can live for much longer. This constrained mutability is, at least for me, an acceptable tradeoff.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top