This is a part of this post:
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.
static int sum(List<Integer> nums) {
int result = 0;
for (int num : nums)
result += num;
return result;
}
sum
like this: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.
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();
}
hasEggRotten
.// 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;
}
getEggLayingDate
felt there is an opportunity to optimize his DB heavy component and so implemented caching like this.static final Map<Integer, Date> eggLayingDateCacheById =
new HashMap<>(); // Cache
Date getEggLayingDate(int eggId) {
return eggLayingDateCacheById
.computeIfAbsent(eggId, this::queryEggLayingDateFromDB);
}
calculateAge
component which has no changes!// Dependent component - 2
long calculateEggAge(int eggId, Date today) {
return today.getDate() - getEggLayingDate(eggId).getDate(); // What did I do? 😿
}
It’s not just the Mutable objects but Java has pointers all around. References are writable by default.
Looking at the numbers of views, up-votes and bookmarks, I am sure a lot of developers have been bitten by this.
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.
Record
types from Java 16Some prevailing arguments about Immutability
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! 😉)
String
, No coincidence that it’s Immutable.Date
with immutable LocalDate
.Record
types and a concise Stream operation toList
for UnmodifiableList.Any many more. The tide’s turning! 🌊