Spec-Zone.ru › Kotlin 1.9

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

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

  • Не добавляйте аргументы в существующие функции API

  • Не используйте классы данных в API

  • Не сужайте типы возвращаемых значений

Рассмотрите следующие варианты:

  • Аннотация @PublishedApi

  • Аннотация @RequiresOptIn

  • Режим явного API

Узнайте о инструментах, предназначенных для обеспечения обратной совместимости.

Определение обратной совместимости

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

Применительно к API существует по меньшей мере три типа совместимости:

  • Исходный код

  • Поведенческая

  • Бинарная

Подробнее о типах совместимости

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

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

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

Возможно нарушение бинарной совместимости без нарушения исходной совместимости, и наоборот.

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

END_OF_DOCUMENT_MARKER ```

Рекомендации по тому, чего делать не следует

Не добавляйте аргументы в существующие функции API

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

Нарушение обратной совместимости продемонстрировано ниже на примере двух классов: lib.kt представляющего «библиотеку», и client.kt представляющего «клиента» этой «библиотеки». Такая конструкция для библиотек и их клиентов распространена в реальных приложениях. В этом примере «библиотека» имеет одну функцию, которая вычисляет пятый член последовательности Фибоначчи. Файл lib.kt содержит:

fun fib() = … // Returns the fifth element

Давайте вызовем эту функцию из другого файла, client.kt:

fun main() {
    println(fib()) // Returns 3
}

Давайте скомпилируем классы:

kotlinc lib.kt client.kt 

Компиляция приводит к двум файлам: LibKt.class и ClientKt.class.

Давайте вызовем клиента, чтобы убедиться, что всё работает:

$ kotlin ClientKt.class
3

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

lib.kt:

fun fib(numberOfElement: Int = 5) = … // Returns requested member

Давайте перекомпилируем только «библиотеку»: kotlinc lib.kt.

Давайте запустим «клиента»:

$ kotlin ClientKt.class

Результат:

Exception in thread "main" java.lang.NoSuchMethodError: 'int LibKt.fib()'
       at LibKt.main(fib.kt:2)
       at LibKt.main(fib.kt)
       …

Возникла ошибка NoSuchMethodError, потому что сигнатура функции fib() изменилась после компиляции.

Если вы перекомпилируете client.kt, всё заработает снова, потому что он будет знать о новой сигнатуре. В этом примере бинарная совместимость была нарушена, сохранив при этом совместимость исходного кода.

Дополнительная информация о произошедшем с помощью декомпиляции

Это объяснение специфично для JVM.

Давайте вызовем javap для класса LibKt до изменений:

❯ javap LibKt
Compiled from "lib.kt"
public final class LibKt {
 public static final int fib();
}

И после изменений:

❯ javap LibKt
Compiled from "lib.kt"
public final class LibKt {
 public static final int fib(int);
 public static int fib$default(int, int, java.lang.Object);
}

Метод с сигнатурой public static final int fib() был заменён новым методом с сигнатурой public static final int fib(int). В то же время, прокси-метод fib$default делегирует выполнение методу fib(int). Для JVM есть способ обойти это: нужно добавить аннотацию @JvmOverloads. Для многоплатформенных проектов такого обхода нет.

Не используйте классы данных в API

Классы данных заманчивы, потому что они короткие, лаконичные и предоставляют некоторые удобные функции «из коробки». Однако из-за некоторых особенностей работы с классами данных лучше не использовать их в API библиотек. Практически любое изменение делает API несовместимым с предыдущими версиями.

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

Во-первых, соображения из предыдущего раздела, не добавляйте аргументы в существующие функции API, также применяются к конструктору, так как он также является методом. Во-вторых, даже если вы добавите вторичные конструкторы, это не решит проблему совместимости. Давайте рассмотрим следующий класс данных:

data class User(
    val name: String,
    val email: String
)

Например, со временем вы понимаете, что пользователи должны пройти процедуру активации, поэтому вы хотите добавить новое поле «активный», со значением по умолчанию «true». Это новое поле должно позволить существующему коду работать в основном без изменений.

Как уже обсуждалось в разделе выше, вы не можете просто добавить новое поле таким образом:

data class User(
    val name: String,
    val email: String,
    val active: Boolean = true
)

Потому что это изменение не бинарно совместимо.

Давайте добавим новый конструктор, который принимает только два аргумента и вызывает первичный конструктор с третьим аргументом по умолчанию:

data class User(
    val name: String,
    val email: String,
    val active: Boolean = true
) {
    constructor(name: String, email: String) :
            this(name, email, active = true)
}

В этот раз есть два конструктора, и сигнатура одного из них совпадает с конструктором класса до изменения:

public User(java.lang.String, java.lang.String);

Но проблема не в конструкторе – она в функции copy. Её сигнатура изменилась с:

public final User copy(java.lang.String, java.lang.String);

До:

public final User copy(java.lang.String, java.lang.String, boolean);

И это сделало код не бинарно совместимым.

Конечно, можно просто добавить свойство внутри класса данных, но это лишит его всех преимуществ быть классом данных. Поэтому лучше не использовать классы данных в вашем API, потому что практически любое изменение в них нарушает исходную, бинарную или поведенческую совместимость.

Если вам необходимо использовать класс данных по какой-либо причине, вам нужно переопределить конструктор и метод copy(). Кроме того, если вы добавите поле в тело класса, вам нужно переопределить методы hashCode() и equals().

Всегда является изменением, нарушающим совместимость, менять порядок аргументов из-за методов componentX(). Это нарушает совместимость исходного кода и, вероятно, нарушит и бинарную совместимость.

Не сужайте типы возвращаемых значений

Иногда, особенно когда вы не используете явный режим API, объявление типа возвращаемого значения может неявно измениться. Но даже если это не так, вы можете захотеть сузить сигнатуру. Например, вы можете понять, что вам нужна индексация элементов вашего набора и захотите изменить тип возвращаемого значения с Collection на List. Расширение типа возвращаемого значения обычно нарушает совместимость исходного кода; например, преобразование с List на Collection нарушает весь код, использующий индексацию. Сужение типов возвращаемых значений обычно является изменением, сохраняющим совместимость исходного кода, но оно нарушает бинарную совместимость, и этот раздел описывает, как.

Рассмотрим функцию библиотеки в файле library.kt:

public fun x(): Number = 3

И пример её использования в файле client.kt:

fun main() {
    println(x()) // Prints 3
}

Давайте скомпилируем его с kotlinc library.kt client.kt и убедимся, что всё работает:

$ kotlin ClientKt
3

Давайте изменим тип возвращаемого значения функции "библиотеки" x() с Number на Int:

fun x(): Int = 3

И перекомпилируем только клиента: kotlinc client.kt. ClientKt больше не работает должным образом. Он больше не печатает 3 и вместо этого выбрасывает исключение:

Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.Number Library.x()'
    	at ClientKt.main(call.kt:2)
    	at ClientKt.main(call.kt)
    	…

Это происходит из-за следующей строки в байткоде:

0: invokestatic  #12 // Method Library.x:()Ljava/lang/Number;

Эта строка означает, что вы вызываете статический метод x(), возвращающий тип Number. Но такого метода больше нет, и поэтому была нарушена бинарная совместимость.

Аннотация @PublishedApi

Иногда вам может потребоваться использовать часть внутреннего API для реализации встроенных функций. Вы можете сделать это с помощью аннотации @PublishedApi. Части кода, помеченные аннотацией @PublishedApi, следует рассматривать как части публичного API, и поэтому следует быть внимательными к их обратной совместимости.

Аннотация @RequiresOptIn

Иногда вы можете захотеть поэкспериментировать с вашим API. В Kotlin есть хороший способ определить, что какой-то API нестабилен – с помощью аннотации @RequiresOptIn. Однако имейте в виду следующее:

  1. Если вы долгое время не меняли часть вашего API и она стабильна, вы должны пересмотреть использование аннотации @RequiresOptIn.

  2. Вы можете использовать аннотацию @RequiresOptIn для определения различных гарантий для разных частей API: предварительный просмотр, экспериментальный, внутренний, тонкий или альфа, бета, RC.

  3. Вы должны явно определить, что означает каждый уровень, написать комментарии KDoc и добавить сообщение об ошибке.

Если вы зависите от API, требующего opt-in, не используйте аннотацию @OptIn. Вместо этого используйте аннотацию @RequiresOptIn, чтобы пользователь мог сознательно выбрать, какой API использовать, а какой – нет.

Ещё одним примером аннотации @RequiresOptIn является ситуация, когда вы хотите явно предупредить пользователей об использовании какого-либо API. Например, если вы поддерживаете библиотеку, которая использует Kotlin reflection, вы можете пометить классы в этой библиотеке аннотацией @RequiresFullKotlinReflection.

END_OF_DOCUMENT_MARKER

Режим явного API

Вам следует стараться поддерживать ваш API максимально прозрачным. Чтобы принудительно сделать API прозрачным, используйте режим явного API.

Kotlin предоставляет вам широкую свободу в написании кода. Можно опустить определения типов, объявления видимости или документацию. Режим явного API обязывает вас, как разработчика, сделать неявные вещи явными. С помощью ссылки выше вы можете узнать, как его включить. Давайте попробуем понять, почему он может вам понадобиться:

  1. Без явного API легче нарушить обратную совместимость:

    // version 1
    fun getToken() = 1
    
    // version 1.1
    fun getToken() = "1"
    

    Тип возвращаемого значения getToken() изменяется, но вам даже не нужно обновлять сигнатуру, чтобы сломать код пользователей. Они ожидают получить Int, но получают String.

  2. То же самое относится к видимости. Если функция getToken() имеет видимость private, то обратная совместимость не нарушается. Но без явного объявления видимости неясно, должен ли пользователь API иметь к ней доступ. Если должен, это должно быть объявлено как public и документировано; в этом случае изменение нарушит обратную совместимость. В противном случае это должно быть определено как private или internal, и это изменение не будет нарушающим.

Инструменты, предназначенные для обеспечения обратной совместимости

Обратная совместимость — это важный аспект разработки программного обеспечения, так как она гарантирует, что новые версии библиотеки или фреймворка могут использоваться с существующим кодом без возникновения проблем. Поддержание обратной совместимости может быть сложной и трудоемкой задачей, особенно при работе с большой кодовой базой или сложными API. Ручное управление ею затруднено, и разработчики часто должны полагаться на тестирование и ручное изучение, чтобы убедиться, что изменения не сломают существующий код. Для решения этой проблемы JetBrains создали валидатор бинарной совместимости, а также существует другое решение: japicmp.

В настоящее время оба работают только для JVM.

Оба решения имеют свои преимущества и недостатки. japicmp работает для любого языка JVM и является как инструментом командной строки, так и плагином системы сборки. Однако он требует наличия как старого, так и нового приложений, упакованных в файлы JAR. Это не так просто использовать, когда у вас нет доступа к старым сборкам вашей библиотеки. Кроме того, japicmp предоставляет информацию о изменениях в метаданных Kotlin, которые вам, возможно, не понадобятся (потому что формат метаданных не указан и считается используемым только для внутренних целей Kotlin).

Валидатор бинарной совместимости работает только как плагин Gradle и находится на уровне стабильности Alpha. Ему не нужен доступ к файлам JAR. Ему нужны только конкретные дампы предыдущего API и текущего API. Он способен собирать эти дампы самостоятельно. Подробнее об этих инструментах см. ниже.

Валидатор бинарной совместимости

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

Этот инструмент может сэкономить значительное количество времени и усилий, которые вы бы иначе потратили на ручное тестирование и проверку. Он также может помочь предотвратить проблемы, которые могут возникнуть из-за нарушений обратной совместимости в API. В конечном счете, это может привести к лучшему пользовательскому опыту, поскольку пользователи смогут полагаться на стабильность и совместимость библиотеки или фреймворка.

japicmp

Если вы нацеливаетесь только на JVM как платформу, вы также можете использовать japicmp. japicmp работает на другом уровне, чем валидатор бинарной совместимости: он сравнивает два файла jar — старый и новый — и сообщает о несовместимостях между ними.

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

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(a: Int, b: Int): Int = a * b
}

Если вы добавите новый метод без нарушения совместимости, как в этом примере:

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(a: Int, b: Int): Int = a * b
    fun divide(a: Int, b: Int): Int = a / b
}

Тогда japicmp сообщит об этом изменении:

Output of japicmp checking compatibility

Это просто изменение аннотации @Metadata, что не очень интересно, но japicmp не зависит от языка JVM и должен сообщать обо всём, что видит.

Последнее изменение: 25 июля 2023 г.
Отладка Начало работы с Kotlin/Native в IntelliJ IDEA

© 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-backward-compatibility.html

Spec-Zone.ru

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