Overfullstack Overfullstack
Design

Mutation is Unholy

Mutation causes Quantum Entanglement

This is a part of this post: Huh? to Aha! - A Refactoring Story

The Entanglement

Mutability causes a lot of problems, especially the Shared mutable state on a Shared Codebase. Let’s focus on how it hinders the goal of Component Isolation.

Mutable Objects as Input params

static int sum(List<Integer> nums) {
int result = 0;
for (int num : nums)
result += num;
return result;
}
static int sumAbsolute(List<Integer> nums) {
for (int i = 0; i < nums.size(); i++) {
nums.set(i, Math.abs(nums.get(i)));
}
return sum(nums); // DRY
}
static void client() {
var nums = Arrays.asList(-2, 5, -6);
System.out.println(sumAbsolute(nums));
}
static void client() {
var nums = Arrays.asList(-2, 5, -6);
System.out.println(sumAbsolute(nums));
System.out.println(sum(nums)); // 13 👺
}
static int sumAbsolute(List<Integer> nums) {
for (int i = 0; i < nums.size(); i++) {
nums.set(i, Math.abs(nums.get(i))); // Latent Bug 🐞
}
return sum(nums); // DRY
}
static void client() {
List<integer> nums = Arrays.asList(-2, 5, -6);
System.out.println(sumAbsolute(nums));
System.out.println(sum(nums)); // 😬
}

Because the sumAbsolute received a reference to a mutable object, it assumed it as a license to do any mutations on it. To me the mistake lies with the client, who passes-around a mutable object reference. This mutable object acts as an invisible string, coupling the components sumAbsolute, client and sum. Thus, Mutable objects as Input params are Unholy for isolation.

Mutable Objects as Return types

Well, that’s even more dangerous. Let’s see with an example: This is a function, which takes an eggId and fetches its laying date by doing a heavy DB operation.

Date getEggLayingDate(int eggId) {
// heavy operation
return queryEggLayingDateFromDB(eggId);
}
// Dependent component - 1
boolean isLaidInFirstHalf(int eggId) {
var layingDate = getEggLayingDate(eggId);
if (layingDate.getDate() < 15) {
return true;
}
return false;
}
// Dependent component - 2
int calculateEggAge(int eggId, Date today) {
return today.getDate() - getEggLayingDate(eggId).getDate();
}
// Dependent component - 1
boolean isLaidInFirstHalf(int eggId) {
var layingDate = getEggLayingDate(eggId);
if (layingDate.getDate() < 15) {
// It's just logging, let's reuse the same Date obj for month and year
layingDate.setDate(15);
logger.info("This egg was laid before: " + layingDate);
return true;
}
return false;
}
static final Map<Integer, Date> eggLayingDateCacheById =
new HashMap<>(); // Cache
Date getEggLayingDate(int eggId) {
return eggLayingDateCacheById
.computeIfAbsent(eggId, this::queryEggLayingDateFromDB);
}
// Dependent component - 2
long calculateEggAge(int eggId, Date today) {
return today.getDate() - getEggLayingDate(eggId).getDate(); // What did I do? 😿
}

References Everywhere

It’s not just the Mutable objects but Java has pointers all around. References are writable by default.

pointers-everywhere

Looking at the numbers of views, up-votes and bookmarks, I am sure a lot of developers have been bitten by this.

But why is Mutability Predominant in Java code?

Because historically it has been the default mode in Java, and defaults are powerful. We all heard Google pays Apple a fat cheque every year just to keep google as default search engine. People seldom change defaults.

It takes Discipline to beat the Default

Some Quick wins 🍒

Anti-Immutables

Some prevailing arguments about Immutability

Isn’t Immutability only for Multi-threading?

concurrent-mind

Immutable Objects doesn’t fit my Imperative style?

Mutation and imperative are super good friends, and one likes to be with the other.

void mutableFn() {
var mutableList = Arrays.asList("a", "b", "c");
mutateList(mutableList);
}
List<String> mutateList(List<String> list) {
for (var i = 0; i < list.size(); i++) {
list.set(i, list.get(i).toUpperCase());
}
return list;
}

But if you use Immutable objects, you need to replace your Imperative mutations with Declarative transformations.

void immutableFn() {
final var immutableList = List.of("a", "b", "c");
transformList(immutableList);
}
List<String> transformList(final List<String> list) {
// `toList()` is new in Java 16 to collect Stream into UnmodifiableList.
return list.stream().map(String::toUpperCase).toList();
}
void immutableFn() {
final var immutableList = List.of("a", "b", "c");
// ! Throws UnsupportedOperationException ⛔️
mutateList(immutableList);
}

Immutability forces Transformation (Now I don’t have to tell you, who is the wife and who is the husband! 😉)

Doesn’t Immutability affect Perf?

Java’s embracing Immutability, slowly

Any many more. The tide’s turning! 🌊

My Talks on this

Back to all articles