Пару слов о value классах.

Пару слов о value классах.

Dmitry Tsyvtsyn

В объектно-ориентированных языках нам приходиться оперировать классами, это очень удобно в прикладных задачах, например: нужно реализовать логику корзины с продуктами, создаём новый класс для корзины и продукта, кладём в корзину нужные товары и отправляем на оплату, всё очень просто и логично, но предположим, что нам нужно создать класс для прогресса, где корректными значениями считаются числа от 0 до 100, у нас появляется компромисс использовать класс, в котором инкапсулированы проверки на корректность, но при этом есть дополнительный оверхед вследствие работы сборщика мусора или использовать примитивное значение типа Int, но при этом минимизирована инкапсуляция:

class Progress(val value: Int) {

init {
require(value >= MIN_VALUE && value <= MAX_VALUE) {
"$value must be from $MIN_VALUE to $MAX_VALUE "
}
}
 
companion object {
const val MIN_VALUE = 0
const val MAX_VALUE = 100
}
}

fun main() {
/*
проверки инкапсулированы, но есть оверхед
при создании объекта класса Progress
*/
val progress1 = Progress(100)

/*
приходиться добавлять внешние проверки,
при этом нет оверхеда как в первом случае
*/
val progress2 = 100
if (progress2 >= 0 && progress2 <= 100) ..
}

К счастью в Kotlin есть такая штука как value classes, которые позволяют избавиться от оверхеда и использовать преимущества инкапсуляции классов:

// аннотация нужна если Kotlin выполняется на JVM, как в случае с Android
@JvmInline
value class Progress(val value: Int) {

init {
require(value >= MIN_VALUE && value <= MAX_VALUE) {
"$value must be from $MIN_VALUE to $MAX_VALUE "
}
}
 
companion object {
const val MIN_VALUE = 0
const val MAX_VALUE = 100
}

}

Принцип работы value класса сводится к тому, что он содержит только одно значение внутри себя и это значение используется напрямую вместо выделения памяти на объект самого value класса, но это происходит не всегда:

// всё ок, используется примитивное значение 100
val progress = Progress(100)
println(progress)

// в скомпилированном виде это выглядит так:
val progress = 100
println(progress)

/*
не ок, так как примитивное значение не может
быть использовано в дженериках, поэтому
будет создан объект класса Progress
*/
val list = mutableListOf<Progress>()
list.add(Progress(100))

Это очень похоже на реализацию примитивных типов Kotlin в JVM:

// используется примитивный тип Int
val progress = 25

// используется обёртка над примитивным типом java.lang.Integer
val factorials = mutableListOf<Int>()
factorials.add(1)
factorials.add(2)
factorials.add(6)

Ладно, идём дальше, предположим у нас есть код вот такого репозитория:

class FavouritesRepository(...) {

fun saveFavourite(model: FavouriteModel) {
if (model.id > -1) {
database.update(...)
} else {
database.insert(...)
}
}

}

Можно конечно создать отдельный метод для обновления уже существующей модели и отдельный для добавления новой, но хотелось бы чтобы репозиторий, отвечающий за хранения данных сам мог разруливать такие штуки, поэтому для искапсуляции проверки id'шника создадим новый value класс:

// в Android код выполняется на JVM
@JvmInline
value class IdLong(val value: Long) {

// проверки на пустой id'шник
val isEmpty: Boolean
get() = value == MIN_POSSIBLE_VALUE
val isNotEmpty: Boolean
get() = !isEmpty

init {
require(value >= MIN_POSSIBLE_VALUE) {
"id must not be less than $MIN_POSSIBLE_VALUE"
}
}

companion object {
private const val MIN_POSSIBLE_VALUE = -1L
// значение по умолчанию
val Empty = IdLong(MIN_POSSIBLE_VALUE)
}

}

Кроме добавления проверок на корректность значения вы также можете создавать значения по умолчанию, например Empty для пустых id'шников, обновим код в репозитории:

class FavouritesRepository(...) {

fun saveFavourite(model: FavouriteModel) {
if (model.id.isNotEmpty) {
database.update(...)
} else {
database.insert(...)
}
}

}

Ну красиво, разве нет? (разумеется)

Помимо прикладного кода, value классы активно используются в библиотеках, например в Jetpack Compose для представления цвета:

@Immutable
@kotlin.jvm.JvmInline
value class Color(val value: ULong) {

...

}

ULong кстати тоже является value классом, который позволяет использовать число типа Long как беззнаковое, кстати тут забавный случай получается: value класс, у которого значением является другой value класс, на самом деле в value класс можно спокойно положить любой возможный тип, не только примитивный, логика останется прежней: будет использовано значение напрямую если есть такая возможность без создания дополнительного объекта самого value класса.

Для дополнительного чтива однозначно рекомендую официальный сайт

Пишите в комментах ваше мнение и всем хорошего кода!

Report Page