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.
Make a habit to use final before var and function params to guard your references.
Follow Immutable strategy for POJOs from Oracle’s Documentation
(Pre Java 16) Auto-generate Immutable version of your POJO using:
Use Record
types from Java 16
Some 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! 😉)
Immutability may lead to creating more objects. But let’s see what Oracle says:
ctrl-c + ctrl-v from Oracle’s Documentation
Period!
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! 🌊