Повышение читаемости кода с помощью расширений Kotlin

Автор оригинала: Meghan Mehta
  • Перевод

В преддверии старта курса "Kotlin Backend Developer" приглашаем будущих студентов и всех желающих посмотреть открытый урок на тему "Пересмотр «12 факторов»: создаём современный микросервис на Kotlin".

Также делимся с вами традиционным переводом полезного материала.


Терминология Kotlin: функции-расширения и свойства-расширения

Когда вы использовали какой-либо API-интерфейс, хотелось ли вам добавить в него новые функции или свойства?

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

К счастью, на помощь приходит Kotlin с функциями-расширениями и свойствами-расширениями. Они позволяют добавлять в класс нужный функционал без необходимости использовать наследование или создавать функцию, принимающую экземпляр класса в качестве параметра. В Android Studio эти расширения видны при использовании функции автозавершения кода, в отличие от соответствующего аналога в языке Java. Расширения можно использовать в сторонних библиотеках, Android SDK или пользовательских классах.

Читайте дальше, если хотите узнать, как повысить читаемость вашего кода с помощью расширений!

Использование функций-расширений

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

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

data class Dog(val name: String, val breed: String, val age: Int)

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

Вы можете вызвать функцию printDogInformation() так же, как вы вызываете любую другую функцию в классе Dog.

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun main() {
  val dog = Dog("Jen", "Pomeranian", 13)
  dog.printDogInformation()
}

Вызов функций-расширений из кода на языке Java

Функции-расширения не являются частью расширяемого класса, поэтому при попытке вызвать их из Java мы не найдем их среди методов расширяемого класса. Как мы увидим позже, расширения декомпилируются в статические методы файла, в котором вы их определили, и получают в качестве входного параметра экземпляр расширяемого класса. Вот как бы выглядел вызов функции-расширения printDogInformation() из Java:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

DogExtensionKt.printDogInformation(dog);

Функции-расширения для типов, допускающих неопределенное значение

Расширения можно также использовать для работы с типами, допускающими неопределенное значение (nullable). Вместо того чтобы делать проверку на null перед вызовом функции-расширения, мы можем создать функцию-расширение для nullable-типа и реализовать проверку на null в коде этой функции. Вот так будет выглядеть функция printInformation(), использующая тип, допускающий неопределенное значение.

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun Dog?.printInformation() {
  if (this == null){
    println("No dog found")
    return
  }
  println("Meet ${this.name} a ${this.age} year old ${this.breed}")
}

Как видите, не нужно делать проверку на null перед вызовом функции printInformation().

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun main() {
  val dog : Dog? = null
  dog.printInformation() // prints "No dog found"
}

Использование свойств-расширений

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

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

val Dog.isReadyToAdopt: Boolean
get() = this.age > 1

Вы можете обратиться к этому свойству-расширению так же, как вы обращаетесь к любому другому свойству в классе Dog.

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

fun main() {
  val dog1 = Dog("Jen", "Pomeranian", 13)
  if(dog1.isReadyToAdopt){
    print("${dog1.name} is ready to be adopted")
  }
}

Переопределение функций-расширений

Невозможно переопределить существующую функцию-член класса. Если определить функцию-расширение с такой же сигнатурой, что и у существующей функции-члена класса, то всегда будет вызываться функция-член, так как то, какая именно функция вызывается, зависит от объявленного статического типа переменной, а не от типа значения данной переменной во время выполнения кода. Например, нельзя расширить функцию toUppercase(), применяемую к строковому типу (String), но можно расширить функцию convertToUppercase().

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

Внутреннее устройство расширений

Мы можем декомпилировать функцию printDogInformation() в Android Studio. Для этого нужно выбрать в меню пункт Tools/Kotlin/Show Kotlin Bytecode (Инструменты/Kotlin/Показать байт-код Kotlin) и нажать кнопку Decompile (Декомпиляция). В декомпилированном виде метод printDogInformation() будет выглядеть так:

<!-- Copyright 2019 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

public static final void printDogInformation(@NotNull Dog $this$printDogInformation) {
  Intrinsics.checkParameterIsNotNull($this$printDogInformation, "$this$printDogInformation");
  String var1 = "Meet " + $this$printDogInformation.getName() + ", a " + $this$printDogInformation.getAge() + " year old " + $this$printDogInformation.getBreed();
  boolean var2 = false;
  System.out.println(var1);
}

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

Заключение

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

Что следует помнить:

  • Расширения преобразуются в статические функции.

  • Функциям-членам класса всегда отдается предпочтение.

  • Возьмите собаку из приюта!

Успехов в программировании!


Узнать подробнее о курсе "Kotlin Backend Developer".

Посмотреть открытый урок на тему
"Пересмотр «12 факторов»: создаём современный микросервис на Kotlin".

ЗАБРАТЬ СКИДКУ

OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 0

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

Самое читаемое