В процессе написания программ мы часто сталкиваемся с данными, для которых допустим только ограниченный набор значений. Например, возраст, который не может быть отрицательным или email, который может иметь только определенный формат строки. При использовании примитивных типов (Int
, String
) приходится писать различные валидаторы, поскольку такие типы:
- могут представлять все что угодно
- могут содержать все что угодно
Тут еще можно упомянуть антипаттерн Primitive Obsession.
Если у нас есть некоторая функция, которая принимает в аргументы имя, email и возраст, описанные примитивными типами:
def foo(name: String, email: String, age: Int) = {
// что-то делаем с name, email и age
}
Пользователь или мы сами можем случайно передать ей некорректные значения.
val result = foo("???", "", -20) // не ок
Добавление валидации в самом примитивном виде может выглядеть как то так.
def foo(name: String, email: String, age: Int) = {
if (!validateName(name) || !validateEmail(email) || !validateAge(age))
// Ошибка валидации
// Если валидация прошла успешно, то используем name, email и age
}
Теперь функция foo
делает не только то, для чего она действительно была создана, но еще и ответственна за то, чтобы в ней производилась валидация (тем самым нарушая первый принцип SOLID).
Может помогут псевдонимы типов (Type alias
)?
Псевдонимы типов не помогут, поскольку они так же продолжают представлять примитивные типы.
type Name = String
type Email = String
type Age = Int
def foo(name: Name, email: Email, age: Age) = ???
val name: Name = "Mark"
val email: Email = "mark@email.com"
val age: Age = 42
foo(name, email, age) // ок
foo(email, name, age) // вообще не ок
И компилятор не сможет сообщить, что мы допустили ошибку, случайно поменяв email
и name
местами.
А value class-ы?
Обернув примитивные типы в кейс-классы (тем самым создав спец. типы), мы не можем просто так взять и подсунуть Name
вместо Email
.
final case class Name(value: String) extends AnyVal
final case class Email(value: String) extends AnyVal
final case class Age(value: Int) extends AnyVal
def foo(name: Name, email: Email, age: Age) = ???
foo(Name("Mark"), Email("mark@email.com"), Age(42)) // ок
foo(Email("mark@email.com"), Name("Mark"), Age(42)) // ошибка компиляции
// type mismatch;
// found : org.github.ainr.experiments.Main.Email
// required: org.github.ainr.experiments.Main.Name
Но эти типы все еще могут принимать любые значения.
val email = Email("blah blah blah") // не ок
Используем refined (уточненные) типы
Refined - это небольшая библиотека для Scala, позволяющая описать тип с помощью предикатов, ограничивающих набор значений, который может принимать этот тип.
Для использования refined-типов необходимо подключить библиотеку refined добавив следующую строчку в build.sbt
libraryDependencies += "eu.timepit" "circe-refined" % "0.14.1"
И сделать импорт там, где будет выполняться json-преобразование.
import io.circe.generic.auto._, io.circe.parser._
import io.circe.refined._
val json =
"""
|{
| "name": "Martin",
| "email": "martin?email.com", // ошибка
| "age": 55
|}
|""".stripMargin
val decodedFoo: Either[circe.Error, Foo] = decode[Foo](json)
// Left(DecodingFailure(Predicate failed: "martin?email.com".matches("(\w)+@([\w\.]+)")., List(DownField(email))))
Собственно, если json будет содержать некорректные значения, то мы в результате получим Either
с ошибкой декодирования (DecodingFailure
).
Неполный список доступных расширений для интеграции различных библиотек с refined-типами приведен в документации.
И кстати, преобразование примитивных типов в refined-типы в рантайме происходит примерно следующим способом.
import eu.timepit.refined.api.RefType
import eu.timepit.refined.api.Refined
import eu.timepit.refined.string._
type Email = String Refined MatchesRegex["""(\w)+@([\w\.]+)"""]
val badEmail = "bad email"
val email = RefType.applyRef[Email](badEmail)
Вместо заключения
Целью этого небольшого поста ставил обозначить проблему, для решения которой были созданы refined-типы и продемонстрировать некоторые возможности библиотеки.
Более подробно с примерами и списком предикатов можно ознакомиться в гитхабе проекта.