Псевдонимы для типов и value-классы
Почти бесплатно повысить читаемость кода можно за счет псевдонимов типов (type aliases). Например, можно заменить в бизнес-логике безликий UUID
на UserId
. Есть это почти во всех языках: typedef
или даже #define
в С, using
в C++ и C#, type alias
в Kotlin, type
в Scala/Haskell и даже в питоне, но не в Java. Можно еще использовать локально: импортировать тип с другим именем. Но у псевдонимов есть банальный недостаток: ничто не помешает в UserId
запихать другой UUID
, например ObjectId
.
Чтобы компилятор проверял подобные несоответствия, можно использовать value-типы (они же Single case discriminated union). Обычно это класс-обертка над целевым типом, который имеет единственное поле и при возможности заменяется после компиляции на целевой тип. Например, в Kotlin это будет
@JvmInline
value class UserId(val value: UUID)
В некоторых языках еще можно добавить логику (например, проверку в init
блоке или добавить методы-геттеры). Если value-типов нет, можно достичь примерно того же, используя data-классы с одним полем (а где-то может и вообще разницы не будет).
Очевидная проблема оберток в том, что к содержимому всегда придется обращаться через поле или паттерн-матчинг, а не напрямую (и какой-нибудь делегацией это, увы, не решается). А из-за потенциального оборачивания страдают библиотеки, использующие рефлексию, например, сериализация в JSON. В Scala это решили с помощью opaque-типов: рядом с объявлением класса он работает прозрачно, как type alias, а снаружи — непрозрачно, и доступа к целевому типу нет. При этом там есть и “обычные” value-классы, которые почти такие же, как в Kotlin.
А еще и в Scala и в Kotlin ждут Project Valhalla, чтобы можно было создавать пользовательские типы, доступ к которым осуществляется по значению, а не по ссылке. В комбинации с inline это позволит решить часть проблем с оборачиванием value-типов.