Notes on ImmutabilityВ данной статье автор размышляет на тему иммутабельности данных и что это дает.
По факту, написание кода с учетом иммутабельности делает код:
📌 Легче для обсуждений и понимания
📌 Менее подвержен ошибкам
📌 Потокобезопасен по умолчанию
🔸 Давайте посмотрим на пример с мутабельными данными:
fun decodeToMutablePerson(json: String, city: String, postalCode: String): MutablePerson {
val person = MutablePerson()
person.city = city
person.postalCode = postalCode
decode(json, person)
validatePerson(person)
return person
}
fun decode(json: String, person: MutablePerson) {
// Pretend we decode.
person.name = "name"
person.age = 100
}
fun validatePerson(person: MutablePerson) {
require(!person.name.isNullOrBlank())
requireNotNull(person.age)
require(!person.city.isNullOrBlank())
require(!person.postalCode.isNullOrBlank())
}
class MutablePerson {
var name: String? = null
var age: Int? = null
var city: String? = null
var postalCode: String? = null
}
🔸 В функции decodeToMutablePerson создается объект с мутабельными свойствами и далее в разных функциях меняет их значения. К такому коду много вопросов и самое важное, что теряется безопасность вызова функций
🔸 Гораздо легче будет сразу отрефачить и сделать иммутабельные свойства:
fun decodeToPerson(json: String, city: String, postalCode: String): Person {
val info = decode(json)
val person = Person(info.name, info.age, city, postalCode)
validatePerson(person)
return person
}
fun decode(json: String): PersonInfo {
// Pretend we decode.
return PersonInfo("name", 100)
}
class PersonInfo(val name: String, val age: Int)
fun validatePerson(person: Person) {
require(person.name.isNotBlank())
require(person.city.isNotBlank())
require(person.postalCode.isNotBlank())
}
class Person(val name: String, val age: Int, val city: String, val postalCode: String)
🔸 Давайте посмотрим еще на второй пример:
fun main() {
val numbers = arrayListOf(1, 2, 3, 4)
val filtered = filter(numbers) { it % 2 == 0 }
// What this prints:
// 1) - 1, 2, 3, 4
// 2) - 2, 4
// 3) - something else?
println(filtered)
val mapped = map(numbers) { it + 10 }
// What this prints:
// 1) - 10, 20, 30, 40
// 2) - 12, 14
// 3) - something else?
println(mapped)
}
fun filter(input: MutableList, predicate: (T) -> Boolean): MutableList {
val iterator = input.listIterator()
for (element in iterator) {
if (!predicate(element)) iterator.remove()
}
return input
}
🔸 Как видно, функция не использует принцип неизменяемого кода. Это приводит к 3 ключевым проблемам с функцией фильтра:
📌 Функция выполняет свою работу, напрямую изменяя входные данные. Теперь знаем, что функция println(mapped) не выведет то, что ожидалось
📌 Это будет неожиданно при чтении кода. Иногда непросто заметить такие вещи при чтение пулл реквеста
📌 Еще одна большая проблема в реализации заключается в том, что используется итератор в изменяемом списке. Если есть несколько активных итераторов, итерирующих и мутирующих список одновременно из другого потока, то можно получить ConcurrentModificationException
🔸 Поэтому лучше писать с помощью иммутабельности. К примеру, функция map:
fun map(input: List, transform: (T) -> R): List {
val result = mutableListOf()
for (element in input) {
val r = transform(element)
result.add(r)
}
return result
}
🔸 Использование изменяемых объектов локально внутри функции и только с одной ссылкой вполне допустимо
🔸 Автор не предлагает полностью отказаться от мутабельности. Изменяемость следует использовать только тогда, когда это необходимо