Spec-Zone.ru › Kotlin 1.9

Предсказуемость

В этом разделе приведены следующие рекомендации:

  • Использование интерфейсов-герметиков

  • Скрытие реализаций с помощью герметичных классов

  • Проверка входных данных и состояния

    • Проверка входных данных с помощью функции require()

    • Проверка состояния с помощью функции check()

  • Избегайте массивов в публичных сигнатурах

  • Избегайте varargs

Использование интерфейсов-герметиков

Интерфейсы в вашем API обычно необходимы, когда вам нужно абстрагироваться от реализации. Если вам необходимо использовать интерфейсы, рассмотрите возможность использования герметичных интерфейсов. Это особенно важно, если вы не хотите, чтобы пользователи вашего API расширяли вашу иерархию.

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

Например, типы JSON могут быть шести типов: объект, массив, число, строка, булево и null. Создание универсального interface JsonElement может привести к ошибкам, потому что пользователь может случайно определить новую реализацию JsonElement, что может нарушить ваш код. Вместо этого вы можете сделать interface 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 условие к утверждению:

fun processJson(json: JsonElement) = when (json) {
    is JsonNumber -> { /* Process as a number */ }
    is JsonObject -> { /* Process as an object */ }
    is JsonArray -> { /* Process as an array */ }
    is JsonBoolean -> { /* Process as a boolean */ }
    is JsonString -> { /* Process as a string */ }
    is JsonNull -> { /* Process as null */ }
    // `else` clause is not required because all the cases are covered
}

Скрытие реализаций с помощью герметичных классов

Если у вас есть герметичный интерфейс в вашем API, это не означает, что вы должны раскрывать все его реализации в вашем API. Минимизация обычно предпочтительнее. Если вам нужно избежать утечек абстракций или вы хотите предотвратить расширение интерфейсов пользователями API, рассмотрите возможность использования герметичных классов или интерфейсов для внутренних реализаций.

Например, библиотека, работающая с разными базами данных, может иметь интерфейс ответа базы данных, подобный этому:

sealed interface DBResponse {
    operator fun <T> get(columnName: String): Sequence<T>
}

Раскрытие реализаций этого интерфейса, таких как SQLiteResponse или MongoResponse, пользователям API является утечкой абстракции и усложняет поддержку этого API. В такой библиотеке вы можете обрабатывать только свои реализации DBResponse. Если пользователь передаст свою реализацию DBResponse в метод библиотеки, принимающий ответы, это может вызвать ошибку. Использование герметичных интерфейсов и классов предотвращает это.

Проверка входных данных и состояния

Проверка входных данных с помощью функции require()

Возможны случаи неправильного использования API. Чтобы помочь пользователям правильно работать с вашим API, необходимо как можно раньше проверять входные данные с помощью функции require().

Например, вот простая функция библиотеки, которая сохраняет пользователей в какой-либо внешний API:

fun saveUser(username: String, password: String) {
    api.saveUser(User(username, password))
}

Вы должны выполнять проверку аргументов функции, чтобы убедиться, что все работает как ожидается. Например, проверьте, что username уникально и не пусто, даже если вы уже определили эти ограничения в базе данных:

fun saveUser(username: String, password: String) {
    require(username.isNotBlank()) { "Username should not be blank" }
    require(api.usernameAvailable(username)) { "Username $username is already taken" }
    require(password.isNotBlank()) { "Password should not be blank" }
    require(password.length > 6) { "Password should contain at least 7 letters" }
    require(
        /* Some complex check */
    ) { "..." }

    api.saveUser(User(username, password))
}

Таким образом вы гарантируете, что пользователю не нужно разбираться в сложных стеках исключений, ведущих к базе данных. В случае исключения это будет IllegalArgumentException со осмысленным сообщением, а не общее исключение базы данных.

Если вы реализовали проверку входных данных, вы также должны задокументировать эти проверки.

Проверка состояния с помощью функции check()

Те же рекомендации применимы к проверке внутреннего состояния. Наиболее очевидным примером является InputStream, так как вы не можете читать из закрытого потока ввода.

Рассмотрим класс InputStream с методом readByte() и его использование:

class InputStream : Closeable {
    private var open = true
    fun readByte(): Byte { /* Read and return one byte */ }
    override fun close() {
        // Dispose of the underlying resource
        open = false
    }
}

fun readTwoBytes(inputStream: InputStream): Pair<Byte, Byte> {
    val first = inputStream.use { it.readByte() }
    val second = inputStream.readByte()
    return Pair(first, second)
}

Метод readTwoBytes() должен вызывать IllegalStateException, потому что use{} закрывает поток ввода, и пользователь не должен иметь возможности читать из закрытого потока. Для реализации этого измените код функции readByte().

fun readByte(): Byte {
    check(open) { "Can't read from the already closed stream" }
    // Read and return one byte
}

В примере выше используется функция check(), а не require(). Эти функции вызывают разные исключения: require() вызывает IllegalArgumentException, в то время как check() вызывает IllegalStateException. Эта разница может стать существенной при отладке.

Избегайте массивов в публичных сигнатурах

Массивы всегда изменяемы, а Kotlin построен на безопасных — читаемых или неизменяемых — объектах. Если вам необходимо использовать массивы в вашем API, скопируйте их перед передачей в любое место, чтобы убедиться, что они не были изменены. В качестве альтернативы используйте только читаемые и изменяемые коллекции в соответствии с вашими намерениями. В общем случае лучше избегать использования массивов, и если это необходимо, делать это с особой осторожностью.

Например, перечисления в Kotlin имеют функцию values() , которая возвращает массив всех элементов перечисления. Если массив не копируется, пользователь может переписать элементы:

enum class Test { A, B }

fun main() { Test.values()[0] = Test.B }

Если вы кэшируете значения внутри перечисления, кеш будет повреждён после выполнения вышеуказанного кода. Если значения не кэшируются, это дополнительная накладная стоимость во время выполнения для каждого вызова функции values().

Избегайте varargs

vararg — переменное число аргументов — работает как массив внутри, но элементы массива передаются функции индивидуально, а не весь массив. Эта операция дорогостоящая, потому что происходит многократное копирование одного и того же массива.

Рассмотрим следующий код:

fun printElements(delimiter: String, vararg elements: String) {
    for (i in elements.indices) {
        print(elements[i])
        if (i < elements.lastIndex) print(delimiter)
    }
}

fun printWithSpace(vararg elements: String) {
    printElements(" ", *elements)
}

fun main() {
    printWithSpace("x", "y", "z")
}

Функция printElements() выводит все строки из аргумента vararg elements с разделителем, а функция printWithSpace() вызывает printElements() с разделителем, определённым как пробел. Код выглядит безобидно: вы просто передаёте элементы из printWithSpace() в printElements(). Без оператора разброса *, код не будет компилироваться, но с ним массив фактически копируется перед передачей функции printElements(). Чем длиннее цепочка, тем больше копий создаётся и тем больше неожиданная нагрузка на память.

Что дальше?

Узнайте о следующих API:

  • Отладка

  • Обратная совместимость

Последнее изменение: 25 июля 2023
Читаемость Отладка

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/jvm-api-guidelines-predictability.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API