Immutability from experience: part 2
This is a 3 part series about what I have learnt from applying the immutable design pattern to my work in the recent months. This is born out of my frustration/confusion on this learning journey and hope that I can save someone from going through my confusion.
Disclaimer: I am not an expert on this matter, but I would like to share my thoughts and experience with immutability and why I believe more people should be using it.
If you’re new to the concept of immutability and have not seen Part 1, I would highly recommend you to go check it out first before reading this. Please treat the code examples are pseudo-code as I go back and forth between languages and syntax slips my grasp sometimes.
- Part 1: Why immutable?
- Part 2: Applying it to software [current]
- Part 3: Applying it to infrastructure [coming soon]
Part 2: Applying it to Software
Between the time it took me to write Part 1 and Part 2, I realize that there are many great articles out there that go in depth about how to apply immutability for the various languages out there. I would point people who are looking for in-depth code examples to search there instead. This is aimed to cover the general application of the concept.
- final, constants by default
- naive vs defensive copying
- only constructors and getters
(1) Final, constants by default
Immutable by default, mutable by choice
Apart from thread safety, by ensuring that the variable cannot be changed, your IDE will have an easier time analyzing what the value is supposed to be. I recommend switching over some of your variables to final/const and hover over to see what are the values your IDE return you. You’ll get a pleasant surprise on how this helps with the traceability of code execution.
However, just cause it’s final or const doesn’t mean that the object cannot change. While the reference of the object stays the same, the value of the object can still change if the members of the object are still mutable. Which brings me to my next point.
(2) Naive vs Defensive copying
Naive copying is what we do by default, where we literally do something like.
String a = customObj.getField()
While there is nothing wrong with it, you wouldn’t know if the value of
customObj will change. This is especially true when using a function from a 3rd party library because you have no control over what’s returned. Defensive copying is a pattern you can apply when you want to guarantee that the section of code cannot be modified by anything external.
Deep Copy. Rely on deep copy instead of a shallow copy. Depending on the language, it might either be a shallow, or a deep copy¹. In shallow copy, when you modify the copied object, the original object gets modified as well. We want to avoid this. The diagram below illustrates the difference between the two copy methods. The fruits represent an actual memory address space, and the grey box represents a pointer to that address.
Return a new instance of the object for any kind of modification. For the following example, we have a
Box class 🗃 that contains a list of fruits. If we want to add a new fruit to the list, we create a new instance of Box with the new fruit and return the entire Box. ArrayList is an example of how you can still modify the items inside even though the variable is declared as
(3) Only constructors and getters
Referring to the same block of code…
- Class should be final so you can’t extend from it (so that nothing can override its methods)
- Getters should return a cloned value of itself so nothing else has a direct reference to its value except the instance itself
When your project is built upon the immutability pattern, it allows complex use case scenario without a complete rehaul. For example, undo/redo features, history state, rollbacks are much simpler to implement because all of your immutable objects can just be pushed into a stack and reading from it will return the state of your application exactly as it was moments ago (well it’s not that easy but you get the point).
We want to avoid is side effects in software, because side effects mess with cause and effect in the flow of execution. For example, if I modify apple 🍎, then orange 🍊 should not be affected, because someone might be using that orange and we wouldn’t want it to turn to shiitake mushrooms 🍄.
I probably wouldn’t recommend using all of these techniques by default for all of your projects. It boils down to balance and trade offs, as it definitely takes extra time and effort to implement it well.
What I would recommend is to definitely search for whatever framework or library out there that can do all of this heavy lifting for you where possible. One such example is immutable.js, which works extremely well with the Redux.
Some of the guiding materials I’ve used while writing this article to fact check myself.
 Shallow copy just copies the pointer reference to the object in memory. Deep copy copies the entire object in memory into a separate address space.