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! 🌊