Immutability
Understanding a piece of code is harder if objects can change their state. Let's take a look at the following example.
enum class LightBulbState {
ON {
override fun lightUp(): String = "bright"
},
OFF {
override fun lightUp(): String = "dark"
};
abstract fun lightUp(): String
}
class LightBulb(private var state: LightBulbState) {
fun lightUp(): String {
return state.lightUp()
}
fun setState(newLightBulbState: LightBulbState) {
state = newLightBulbState
}
}
class Room {
private val lightBulb: LightBulb
constructor(lightBulb: LightBulb) {
lightBulb.setState(LightBulbState.OFF)
this.lightBulb = lightBulb
}
}
class App {
fun main() {
val lightBulb = LightBulb(LightBulbState.ON)
val room = Room(lightBulb)
val result = "The room is ${lightBulb.lightUp()}"
}
}
Just looking at the main
method of App
, we would expect result
to be The room is bright
. However, because of the
Room
constructor, the actual content of result
is The room is dark
. To avoid this bad surprise, LightBulb
can become
immutable, meaning its state cannot be modified. Let's take a look how we can achieve this.
enum class LightBulbState {
ON {
override fun lightUp(): String = "bright"
},
OFF {
override fun lightUp(): String = "dark"
};
abstract fun lightUp(): String
}
class LightBulb(private val state: LightBulbState) {
fun lightUp(): String {
return state.lightUp()
}
fun setState(newLightBulbState: LightBulbState): LightBulb {
return LightBulb(newLightBulbState)
}
}
class Room {
private val lightBulb: LightBulb
constructor(lightBulb: LightBulb) {
this.lightBulb = lightBulb.setState(LightBulbState.OFF)
}
}
class App {
fun main() {
val lightBulb = LightBulb(LightBulbState.ON)
val room = Room(lightBulb)
val result = "The room is ${lightBulb.lightUp()}"
}
}
App().main()
The above code differs from the original one for three aspects:
- The
setState
method ofLightBulb
does not modify its state, instead it returns a brand-new instance ofLightBulb
- The constructor of
Room
stores the brand-newLightBulb
returned by the invocation ofsetState
, instead of theLightBulb
passed as a constructor parameter - The
LightBulb
constructor usesval
instead ofvar
(in Kotlin a variable defined asval
cannot be re-assigned)
Now the content of result in the main
method of App
is what we would expect from the beginning: The room is bright
.
Immutability makes it easier to reason about a piece of code and it is a game changer when concurrency
comes into play. To obtain immutability we need to:
- return a new instance of an object instead of modifying its state
- prevent the re-assignment of variables
Unfortunately it can be tricky to write immutable code. Let's take a look at the following piece of example.
class Shelf(private val books: List<String>) {
fun numberOfBooks(): Int {
return books.size
}
}
class App {
fun main() {
val books = mutableListOf("The Secret Adversary", "The Big Four")
val shelf = Shelf(books)
books.add("Giant's Bread")
val result = "The shelf contains ${shelf.numberOfBooks()} books"
}
}
The content of result
in the main
method of App
will be The shelf contains 3 books
. In fact, even if Shelf
uses
val
and does not allow its state to change, the books
list can still be modified by code outside Shelf
. For Shelf
to be immutable, it has to make a local copy of the books
list received in the constructor as follows.
class Shelf {
private val books: List<String>
constructor(booksForShelf: List<String>) {
books = booksForShelf.toList() // toList makes an exact copy
}
fun numberOfBooks(): Int {
return books.size
}
}
class App {
fun main() {
val books = mutableListOf("The Secret Adversary", "The Big Four")
val shelf = Shelf(books)
books.add("Giant's Bread")
val result = "The shelf contains ${shelf.numberOfBooks()} books"
}
}
The content of result
in the main
method of App
is now The shelf contains 2 books
.
As a final note, reasoning about state is hard and that's why it is worth deepen the paradigm of functional programming which removes the concept of state as much as possible.
Recommended reads
- Appreciating Immutability, section 5.3 of 99 bottles of OOP - Sandy Metz
- Make defensive copies when needed, chapter 9 of Effective Java second edition - Joshua Bloch
- Practical Function Programming in Scala - Gabriel Volpe
- Domain Modeling Made Functional - Scott Wlaschin
- CQRS Documents - Greg Young
Teach me back
I really appreciate any feedback about the book and my current understanding of software design.