Предсказуемость
Чтобы создать надежную и удобную библиотеку Kotlin, важно предусмотреть распространенные сценарии использования, обеспечить возможность расширения и гарантировать правильное использование. Следование рекомендациям по настройкам по умолчанию, обработке ошибок и управлению состоянием обеспечивает удобство работы пользователей и сохраняет целостность и качество библиотеки.
Правильное поведение по умолчанию
Ваша библиотека должна предусматривать «счастливый путь» для каждого сценария использования и соответствующим образом задавать настройки по умолчанию. Пользователям не должно требоваться указывать значения по умолчанию, чтобы библиотека работала правильно.
Например, при использовании Ktor HttpClient наиболее распространенный сценарий использования — отправка GET-запроса на сервер. Это можно сделать с помощью приведенного ниже кода, указав только необходимые сведения:
val client = HttpClient(CIO)
val response: HttpResponse = client.get("https://ktor.io/")
Не нужно задавать значения для обязательных HTTP-заголовков или пользовательских обработчиков событий для возможных кодов состояния в ответе.
Если у сценария использования нет очевидного «счастливого пути» или параметру нужно задать значение по умолчанию, но подходящего варианта нет, вероятно, это указывает на недостатки анализа требований.
Предусмотрите возможности расширения
Если нельзя заранее определить правильный выбор, позвольте пользователям указать предпочитаемый подход. Ваша библиотека также должна позволять пользователю использовать собственный подход или стороннее расширение.
Например, при работе с Ktor HttpClient пользователям рекомендуется при настройке клиента установить поддержку согласования содержимого и указать предпочтительные форматы сериализации:
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
}
Пользователи могут выбрать устанавливаемые плагины или создать собственные с помощью отдельного API для определения плагинов клиента.
Кроме того, пользователи могут определять функции и свойства-расширения для типов из библиотеки. Как автор библиотеки, вы можете упростить эту задачу, если будете учитывать расширения при проектировании и обеспечите наличие у типов вашей библиотеки четких основных концепций.
Предотвращайте нежелательные и недопустимые расширения
Пользователи не должны иметь возможности расширять библиотеку способами, которые противоречат ее изначальному замыслу или невозможны в рамках правил предметной области.
Например, при маршалинге данных в JSON и обратно в выходном формате поддерживаются только шесть типов: object, array, number, string, boolean и null.
Если создать открытый класс или интерфейс с именем JsonElement, пользователи смогут создавать недопустимые производные типы, например JsonDate. Вместо этого можно сделать интерфейс JsonElement запечатанным и предоставить реализацию для каждого типа:
sealed interface JsonElement class JsonNumber(val value: Number) : JsonElement class JsonObject(val values: Map<String, JsonElement>) : JsonElement class JsonArray(val values: List<JsonElement>) : JsonElement class JsonBoolean(val value: Boolean) : JsonElement class JsonString(val value: String) : JsonElement object JsonNull : JsonElement
Запечатанные типы также позволяют компилятору проверять, что выражения when являются исчерпывающими, без необходимости использовать оператор else, что повышает удобочитаемость и единообразие.
Не предоставляйте изменяемое состояние
При работе с несколькими значениями ваш API по возможности должен принимать и/или возвращать коллекции только для чтения. Изменяемые коллекции не являются потокобезопасными и усложняют работу библиотеки, делая ее поведение непредсказуемым.
Например, если пользователь изменит изменяемую коллекцию, возвращенную точкой входа API, будет непонятно, меняет ли он структуру реализации или ее копию. Аналогично, если пользователи могут изменять значения в коллекции после ее передачи в библиотеку, будет непонятно, повлияет ли это на реализацию.
Поскольку массивы являются изменяемыми коллекциями, не используйте их в API. Если массивы необходимы, создавайте защитные копии перед передачей данных пользователям. Это гарантирует, что ваши структуры данных останутся неизменными.
Для аргументов vararg компилятор автоматически создает такие защитные копии. При использовании оператора spread для передачи существующего массива в качестве аргумента vararg автоматически создается копия массива.
Это поведение показано в следующем примере:
fun main() {
fun demo(vararg input: String): Array<out String> = input
val originalArray = arrayOf("one", "two", "three", "four")
val newArray = demo(*originalArray)
originalArray[1] = "ten"
//prints "one, ten, three, four"
println(originalArray.joinToString())
//prints "one, two, three, four"
println(newArray.joinToString())
}
Проверяйте входные данные и состояние
Чтобы гарантировать правильное использование библиотеки, проверяйте входные данные и текущее состояние до начала выполнения реализации. Используйте функцию require для проверки входных данных и функцию check для проверки текущего состояния.
Функция require выбрасывает исключение IllegalArgumentException, если условие равно false, в результате чего функция немедленно завершается с соответствующим сообщением об ошибке:
fun saveUser(username: String, password: String) {
require(username.isNotBlank()) { "Username should not be blank" }
require(username.all { it.isLetterOrDigit() }) {
"Username can only contain letters and digits, was: $username"
}
require(password.isNotBlank()) { "Password should not be blank" }
require(password.length >= 7) {
"Password must contain at least 7 characters"
}
/* Implementation can proceed */
}
Сообщения об ошибках должны включать соответствующие входные данные, чтобы помочь пользователям определить причину сбоя. Например, приведенное выше сообщение об ошибке для имен пользователей с недопустимыми символами содержит некорректное имя пользователя. Исключением является случай, когда включение значения в сообщение об ошибке может раскрыть информацию, которую можно злонамеренно использовать для атаки на систему безопасности. Поэтому в сообщении об ошибке, связанном с длиной пароля, не указывается введенный пароль.
Аналогично, функция check выбрасывает исключение IllegalStateException, если условие равно false. Используйте эту функцию для проверки состояния экземпляра, как показано в примере ниже:
class ShoppingCart {
private val contents = mutableListOf<Item>()
fun addItem(item: Item) {
contents.add(item)
}
fun purchase(): Amount {
check(contents.isNotEmpty()) {
"Cannot purchase an empty cart"
}
// Calculate and return amount
}
}
Следующий шаг
В следующей части руководства вы узнаете об удобстве отладки.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/api-guidelines-predictability.html