16

When designing classes to hold your data model I've read it can be useful to create immutable objects but at what point does the burden of constructor parameter lists and deep copies become too much and you have to abandon the immutable restriction?

For example, here is an immutable class to represent a named thing (I'm using C# syntax but the principle applies to all OO languages)

class NamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    
    public NamedThing(NamedThing other)
    {
         this._name = other._name;
    }
    public string Name
    {
        get { return _name; }
    }
}

Named things can be constructed, queried and copied to new named things but the name cannot be changed.

This is all good but what happens when I want to add another attribute? I have to add a parameter to the constructor and update the copy constructor; which isn't too much work but the problems start, as far as I can see, when I want to make a complex object immutable.

If the class contains may attributes and collections, containing other complex classes, it seems to me the constructor parameter list would become a nightmare.

So at what point does a class become too complex to be immutable?

  • I always make an effort to make the classes in my model immutable. If you're having huge, long contructor parameter lists then maybe your class is too big and it can be split down? If your lower-level objects are also immutable and follow the same pattern then your higher-level objects shouldn't suffer (too much). I find it MUCH harder to change an existing class to become immutable than to make a data model immutable when I'm starting from scratch. – Nobody Apr 14 '11 at 10:19
  • 1
    You could look at the Builder pattern suggested in this question: stackoverflow.com/questions/1304154/… – Ant Apr 14 '11 at 10:22
  • Have you looked at MemberwiseClone? You don't have to update the copy constructor for each new member. – kevin cline Apr 14 '11 at 15:15
  • 3
    @Tony If your collections and everything they contain are also immutable, you don't need a deep copy, a shallow copy is sufficient. – mjcopple Apr 14 '11 at 16:24
  • 2
    As an aside, I commonly use "set-once" fields in classes where the class needs to be "fairly" immutable, but not completely. I find this solves the problem of huge constructors, but provides most of the benefits of immutable classes. (namely, your internal class code not having to worry about a value changing) – Earlz Apr 14 '11 at 23:04
23

When they become a burden? Very quickly (specially if your language of choice does not provide sufficient syntactic support for immutability.)

Immutability is being sold as the silver bullet for the multi-core dilemma and all that. But immutability in most OO languages forces you to add artificial artifacts and practices in your model and process. For each complex immutable class you must have an equally complex (at least internally) builder. No matter how you design it, it stills introduces strong coupling (thus we better have a good reason to introduce them.)

It is not necessarily possible to model everything in small non-complex classes. So for large classes and structures, we artificially partition them - not because that makes sense in our domain model, but because we have to deal with their complex instantiation and builders in code.

It is worse still when people take the idea of immutability too far in a general purpose language like Java or C#, making everything immutable. Then, as a result, you see people forcing s-expression constructs in languages that do not support such things with ease.

Engineering is the act of modeling through compromises and trade-offs. Making everything immutable by edict because someone read that everything is immutable in X or Y functional language (a completely different programming model), that is not acceptable. That is not good engineering.

Small, possibly unitary things can be made immutable. More complex things can be made immutable when it makes sense. But immutability is not a silver bullet. The ability to reduce bugs, to increase scalability and performance, those are not the sole function of immutability. It is a function of proper engineering practices. After all, people have written good, scalable software without immutability.

Immutability gets to become a burden really fast (it adds to accidental complexity) if it is done without a reason, when it is done outside of what it make sense in the context of a domain model.

I, for one, try to avoid it (unless I'm working in a programming language with good syntactic support for it.)

  • 6
    luis, have you noticed how the well-written, pragmatically correct answers written with an explanation of simple yet sound engineering principles tend not to get as many votes as those using state-of-the-art coding fads? This is a great, great answer. – Huperniketes Jul 1 '11 at 10:29
  • 3
    Thanks :) I've noticed the rep-trends myself, but that's ok. Fad fanboys churn code that we later get to repair at better hourly rates, hahah :) jk... – luis.espinal Jul 1 '11 at 14:05
  • 4
    Silver bullet? no. Worth the bit of awkwardness in C# / Java (it's not really that bad)? Absolutely. Also, multicore's role in immutability is quite minor... the real benefit is ease of reasoning. – Mauricio Scheffer Feb 13 '12 at 15:00
  • @Mauricio - if you say so (that immutability in Java isn't that bad). Having worked on Java from 1998 till 2011, I'd beg to differ, it is not trivial exempt in simple code bases. However, people have different experiences, and I acknowledge that my POV is not free of subjectivity. So sorry, can't agree there. I do agree, however, with the ease of reasoning being the most important thing when it comes to immutability. – luis.espinal Mar 5 '12 at 15:51
13

I went through a phase of insisting on classes being immutable where possible. Had builders for pretty much everything, immutable arrays, etc, etc. I found the answer to your question is simple: At what point do immutable classes become a burden? Very quickly. As soon as you want to serialize something, you have to be able to deserialize, which means it must be mutable; as soon as you want to use an ORM, most of them insist on properties being mutable. And so on.

I eventually replaced that policy with immutable interfaces to mutable objects.

class NamedThing : INamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    

    public NamedThing(NamedThing other)
    {
        this._name = other._name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

interface INamedThing
{
    string Name { get; }
}

Now the object has flexibility but you can still tell calling code that it shouldn't edit these properties.

  • 8
    Minor quibbles aside, I agree that immutable objects can become a pain in the butt very quickly when programming in an imperative language, but I'm not really sure if an immutable interface really solves the same problem, or any problem at all. The prime reason to use an immutable object is so you can throw it around anywhere at any time and never have to worry about somebody else corrupting your state. If the underlying object is mutable then you don't have that guarantee, especially if the reason you kept it mutable was because various things need to mutate it. – Aaronaught Apr 14 '11 at 22:05
  • 1
    @Aaronaught - Interesting point. I guess it's a psychological thing more than an actual protection. However, your last line has a false premise. The reason for keeping it mutable is more that various things need to instantiate it and populate via reflection, not to mutate once instantiated. – pdr Apr 14 '11 at 22:11
  • 1
    @Aaronaught: The same way IComparable<T> guarantees that if X.CompareTo(Y)>0 and Y.CompareTo(Z)>0, then X.CompareTo(Z)>0. Interfaces have contracts. If the contract for IImmutableList<T> specifies that the values of all items and properties must be "set in stone" before any instance is exposed to the outside world, then all legitimate implementations will do so. Nothing prevents an IComparable implementation from violating transitivity, but implementations which do so are illegitimate. If a SortedDictionary malfunctions when given an illegitimate IComparable,... – supercat Feb 23 '14 at 17:41
  • 1
    @Aaronaught: Why should I trust that implementations of IReadOnlyList<T> will be immutable, given that (1) no such requirement is stated in the interface documentation, and (2) the most common implementation, List<T>, isn't even read-only? I'm not quite clear what's ambiguous about my terms: a collection is readable if the data within can be read. It's read-only if it can promise that the data contained cannot be changed unless some external reference is held by code which would change it. It's immutable if it can guarantee that it cannot be changed, period. – supercat Feb 23 '14 at 18:05
  • 1
    @supercat: Incidentally, Microsoft agrees with me. They released an immutable collections package and notice that they're all concrete types, because you cannot ever guarantee that an abstract class or interface is truly immutable. – Aaronaught Feb 23 '14 at 18:17
8

I don't think there is a general answer to this. The more complex a class is, the harder it is to reason about its state changes, and the costlier it is to create new copies of it. So above some (personal) level of complexity it will become too painful to make/keep a class immutable.

Note that a too complex class, or a long method parameter list are design smells per se, regardless of immutability.

So usually the preferred solution would be to break such a class into multiple distinct classes, each of which can be made mutable or immutable on its own. If this is not feasible, it can be turned mutable.

5

You can avoid the copy problem if you store all of your immutable fields in an inner struct. This is basically a variation of the memento pattern. Then when you want to make a copy, just copy the memento:

class MyClass
{
    struct Memento
    {
        public int field1;
        public string field2;
    }

    private readonly Memento memento;

    public MyClass(int field1, string field2)
    {
        this.memento = new Memento()
            {
                field1 = field1,
                field2 = field2
            };
    }

    private MyClass(Memento memento) // for copying
    {
        this.memento = memento;
    }

    public int Field1 { get { return this.memento.field1; } }
    public string Field2 { get { return this.memento.field2; } }

    public MyClass WithNewField1(int newField1)
    {
        Memento newMemento = this.memento;
        newMemento.field1 = newField1;
        return new MyClass(newMemento);
    }
}
  • I think the inner struct is not necessary. It's just another way of doing MemberwiseClone. – Codism Apr 14 '11 at 22:17
  • @Codism - yes and no. There are times when you might need other members that you don't want to clone. What if you were using lazy evaluation in one of your getters and caching the result in a member? If you do a MemberwiseClone, you'll clone the cached value, then you'll change one of your members that the cached value depends on. It's cleaner to separate the state from the cache. – Scott Whitlock Apr 15 '11 at 13:44
  • It might be worth mentioning another advantage of the inner struct: it makes it easy for an object to copy its state to an object to which other references exist. A common source of ambiguity in OOP is whether a method which returns an object reference is returning a view of an object that might change outside the recipient's control. If instead of returning an object reference, a method accepts an object reference from the caller and copies state to it, ownership of the object will be much clearer. Such an approach doesn't work well with freely-inheritable types, but... – supercat Feb 23 '14 at 17:29
  • ...it can be very useful with mutable data holders. The approach also makes it very easy to have "parallel" mutable and immutable classes (derived from an abstract "readable" base) and have their constructors be able to copy data from each other. – supercat Feb 23 '14 at 17:32
3

You have a couple of things at work here. Immutable data sets are great for multithreaded scalability. Essentially, you can optimize your memory quite a bit so that one set of parameters is one instance of the class--everywhere. Because the objects never change you don't have to worry about synchronizing around accessing its members. That's a good thing. However, as you point out, the more complex the object is the more you need some mutability. I would start with reasoning along these lines:

  • Is there any business reason why an object can change its state? For example, a user object stored in a database is unique based on its ID, but it has to be able to change state over time. On the other hand when you change coordinates on a grid, it ceases to be the original coordinate and so it makes sense to make coordinates immutable. Same with strings.
  • Can some of the attributes be computed? In short, if the other values in the new copy of an object are a function of some core value you pass in, you can either compute them in the constructor or on demand. This reduces the amount of maintenance as you can initialize those values the same way on copy or create.
  • How many values make up the new immutable object? At some point the complexity of creating an object becomes non-trivial and at that point having more instances of the object can become a problem. Examples include immutable tree structures, objects with more than three passed in parameters, etc. The more parameters the more possibility of messing up the order of parameters or nulling out the wrong one.

In languages that only support immutable objects (such as Erlang), if there is any operation that seems to modify the state of an immutable object, the end result is a new copy of the object with the updated value. For example, when you add an item to a vector/list:

myList = lists:append([[1,2,3], [4,5,6]])
% myList is now [1,2,3,4,5,6]

That can be a sane way of working with more complicated objects. As you add a tree node for example, the result is a new tree with the added node. The method in the above example returns a new list. In the example in this paragraph the tree.add(newNode) would return a new tree with the added node. For the users, it becomes easy to work with. For the library writers it becomes tedious when the language doesn't support implicit copying. That threshold is up to your own patience. For the users of your library, the most sane limit I've found is about three to four parameters tops.

  • If one would be inclined to use a mutable object reference as a value [meaning that no references are contained within its owner and never exposed], constructing a new immutable object which holds the desired "changed" contents is equivalent to modifying the object directly, though is likely slower. Mutable objects, however, can also be used as entities. How would one make things that behave like entities without mutable objects? – supercat Feb 23 '14 at 17:24
0

If you have multiple final class members and don't want them to be exposed to all objects who need to create it, you can use the builder pattern:

class NamedThing
{
    private string _name;    
    private string _value;
    private NamedThing(string name, string value)
    {
        _name = name;
        _value = value;
    }    
    public NamedThing(NamedThing other)
    {
        this._name = other._name;
        this._value = other._value;
    }
    public string Name
    {
        get { return _name; }
    }

    public static class Builder {
        string _name;
        string _value;

        public void setValue(string value) {
            _value = value;
        }
        public void setName(string name) {
            _name = name;
        }
        public NamedThing newObject() {
            return new NamedThing(_name, _value);
        }
    }
}

the advantage is that you can easily create a new object with only a different value of a differtent name.

  • I think that your builder being static is not correct. Another thread could change the static name or static value after you set those, but before you call newObject. – ErikE Jul 24 '15 at 22:15
  • Only the builder class is static, but its members are not. This means that for every builder you create, they have their own set of members with corresponding values. The class needs to be static so it can be used and instantiated outside of the containing class (NamedThing in this case) – Salandur Jul 28 '15 at 18:56
  • I see what you're saying, I just envision a problem with this because it doesn't lead a developer to "fall into the pit of success." The fact that it uses static variables means that if a Builder is reused, there is real risk of the thing happening that I mentioned. Someone might be building a lot of objects and decide that since most of the properties are the same, to simply reuse the Builder, and in fact, let's make it a global singleton that is dependency-injected! Whoops. Major bugs introduced. So I think this pattern of mixed instantiated vs. static is bad. – ErikE Jul 29 '15 at 15:54
  • 1
    @Salandur Inner classes in C# are always "static" in the Java inner class sense. – Sebastian Redl Dec 14 '17 at 10:29
0

So at what point does a class become too complex to be immutable?

In my opinion it's not worth bothering to make small classes immutable in languages like the one you are showing. I'm using small here and not complex, because even if you add ten fields to that class and it does really fancy operations on them, I doubt it's going to take kilobytes let alone megabytes let alone gigabytes, so any function using instances of your class can simply make a cheap copy of the whole object to avoid modifying the original if it wants to avoid causing external side effects.

Persistent Data Structures

Where I find personal use for immutability is for big, central data structures that aggregate a bunch of teeny data like instances of the class you're showing, like one that stores a million NamedThings. By belonging to a persistent data structure which is immutable and being behind an interface that only allows read-only access, the elements that belong to the container become immutable without the element class (NamedThing) having to deal with it.

Cheap Copies

The persistent data structure allows regions of it to be transformed and made unique, avoiding modifications to the original without having to copy the data structure in its entirety. That's the real beauty of it. If you wanted to naively write functions that avoid side effects that input a data structure that takes gigabytes of memory and only modifies a megabyte's worth of memory, then you'd have to copy the entire freaking thing to avoid touching the input and return a new output. It's either copy gigabytes to avoid side effects or cause side effects in that scenario, making you have to choose between two unpleasant choices.

With a persistent data structure, it allows you to write such a function and avoid making a copy of the entire data structure, only requiring about a megabyte of extra memory for the output if your function only transformed a megabyte's worth of memory.

Burden

As for the burden, there's an immediate one at least in my case. I need those builders people are talking about or "transients" as I call them to be able to effectively express transformations to that massive data structure without touching it. Code like this:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
     // Transform stuff in the range, [first, last).
     for (; first != last; ++first)
          transform(stuff[first]);
}

... then has to be written like this:

ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
     // Grab a "transient" (builder) list we can modify:
     TransientList<Stuff> transient(stuff);

     // Transform stuff in the range, [first, last)
     // for the transient list.
     for (; first != last; ++first)
          transform(transient[first]);

     // Commit the modifications to get and return a new
     // immutable list.
     return stuff.commit(transient);
}

But in exchange for those two extra lines of code, the function is now safe to call across threads with the same original list, it causes no side effects, etc. It also makes it really easy to make this operation an undoable user action since the undo can just store a cheap shallow copy of the old list.

Exception-Safety or Error Recovery

Not everyone might benefit as much as I did from persistent data structures in contexts like these (I found so much use for them in undo systems and non-destructive editing which are central concepts in my VFX domain), but one thing applicable to just about everyone to consider is exception-safety or error recovery.

If you want to make the original mutating function exception-safe, then it needs rollback logic, for which the simplest implementation requires copying the entire list:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
    // Make a copy of the whole massive gigabyte-sized list 
    // in case we encounter an exception and need to rollback
    // changes.
    MutList<Stuff> old_stuff = stuff;

    try
    {
         // Transform stuff in the range, [first, last).
         for (; first != last; ++first)
             transform(stuff[first]);
    }
    catch (...)
    {
         // If the operation failed and ran into an exception,
         // swap the original list with the one we modified
         // to "undo" our changes.
         stuff.swap(old_stuff);
         throw;
    }
}

At this point the exception-safe mutable version is even more computationally expensive and arguably even harder to write correctly than the immutable version using a "builder". And a lot of C++ developers just neglect exception-safety and maybe that's fine for their domain, but in my case I like to make sure my code functions correctly even in the event of an exception (even writing tests that deliberately throw exceptions to test exception safety), and that makes it so I have to be able to rollback any side effects a function causes halfway into the function if anything throws.

When you want to be exception-safe and recover from errors gracefully without your application crashing and burning, then you have to revert/undo any side effects a function can cause in the event of an error/exception. And there the builder can actually save more programmer time than it costs along with computational time because: ...

You don't have to worry about rolling back side effects in a function which doesn't cause any!

So back to the fundamental question:

At what point do immutable classes become a burden?

They're always a burden in languages that revolve more around mutability than immutability, which is why I think you should use them where the benefits significantly outweigh the costs. But at a broad enough level for big enough data structures, I do believe there are many cases where it's a worthy trade-off.

Also in mine, I only have a few immutable data types, and they're all huge data structures intended to store massive numbers of elements (pixels of an image/texture, entities and components of an ECS, and vertices/edges/polygons of a mesh).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.