- PVSM.RU - https://www.pvsm.ru -
Привет! Данная статья о наболевших проблемах при программировании на Kotlin. В частности, затрону несколько тем, вызывающих больше всего неоднозначности – использование it в лямбда-выражениях, злоупотребление функциями из файла Standard.kt и краткость написания vs. читаемость кода.
Я начал смотреть на Kotlin около года назад (начиная с Milestone 12) и активно применял его для написания своих Android-приложений. После двух лет написания Android-приложений на языке Java писать на Kotlin было глотком свежего воздуха — код был намного компактнее (никаких тебе анонимных классов, появились функциональные фичи), а сам язык намного выразительнее (extension-функции, лямбда-функции) и безопаснее (null safety).
Когда язык вышел в релиз, я без капли сомнения начал писать на нём свой новый проект на работе, попутно расхваливая его своим коллегам (в своей небольшой компании я единственный Android-разработчик, остальные разрабатывают на Java клиент-серверные приложения). Я понимал, что после меня новому члену команды придется учить этот язык, что на мой взгляд в данном случае не являлось проблемой — этот язык очень похож на Java и через 3-5 дней после прочтения официальной документации на нём уже можно начать уверено писать.
Спустя какое-то время я начал замечать, что в некоторых случаях нужно бить себя по рукам и писать более длинный, но понятный код, нежели краткий и менее понятный. Пример:
// Намного лучше читается, когда выход из функции следует сразу за единственным Safe-call ("?."), после чего идет получение имени отдельной строчкой
val user = response?.user ?: return
val name = user.name.toLowerCase()
// Хуже читается, когда сразу несколько разных действий совмещено на одной строчке
val name = response?.user?.name.toLowerCase() ?: return
Так как я был единственным программистом, быстро понял эту закономерность и неявно выработал для себя правило предпочитать читаемость кода его краткости. Всё бы было ничего, пока мы не взяли на стажировку начинающего Android-программиста. Как я и ожидал, после прочтения официальной документации по языку он быстро освоил Kotlin, имея за плечами опыт программирования на Java, но потом стали происходить странные вещи: каждое code review вызывало между нами получасовые (а иногда и часовые) дискуссии на тему того, какие конструкции языка лучше использовать в тех или иных ситуациях. Иными словами, мы начали вырабатывать стиль программирования на Kotlin в нашей компании. Я считаю, что эти дискуссии возникали по той причине, что в документации, являющейся входной точкой в мир Kotlin, не приведено тех самых Best Practices, а именно когда лучше НЕ использовать данные фичи и что лучше использовать вместо этого. Именно поэтому я и решил написать данную статью.
Сразу хочу оговорить, что я не пытаюсь доказать истинность моих утверждений, а пытаюсь обсудить как же всё-такие правильно писать те или иные вещи на Kotlin.
Данная проблема заключается в том, что в Kotlin разрешено не именовать единственный параметр функции обратного вызова. Он по умолчанию будет иметь имя «it». Пример:
/** Интерфейс с методом call, который принимает один параметр и ничего не возвращает */
interface Callback {
fun call(parameter: Any?)
}
fun execute(callback: Callback) {
...
callback(parameter)
...
}
/** Пример вызова. Kotlin позволяет писать как execute { ... }, так и execute({ ... }), выберем более краткий вариант */
execute {
if (it is String) { // Доступ к parameter через переменную it, проверка что он имеет тип String
....
}
....
}
Однако когда мы имеем несколько вложенных функций, может возникнуть путаница:
execute {
execute {
execute {
if (it is String) { // it относится к последнему по вложенности вызову execute
....
}
....
}
}
}
execute {
execute {
execute { parameter ->
if (it is String) { // здесь it относится уже к предпоследнему по вложенности вызову execute, так как параметр последнего имеет другое имя
....
}
....
}
}
}
На небольших фрагментах когда это может не казаться такой проблемой, однако если над кодом работают несколько человек и такая функция с вложенным вызовом имеет 10-15 строчек, то легко потерять, кому же на самом деле принадлежит it на данном уровне вложенности. Ситуация ухудшается, если в каждом уровне вложенности используется имя it для какой-то операции. В этом случае понимание такого кода сильно ухудшается.
executeRequest { // здесь it - это экземпляр класса Response
if (it.body() == null) return
executeDB { // здесь it - это экземпляр класса DatabaseHelper
it.update(user)
executeInBackgroud { // здесь it - это экземпляр класса Thread
if (it.wait()) ...
....
}
}
}
Здесь [1] приведена дискуссия на тему читаемости кода, использующего it. Мое мнение — it сильно помогает сокращать код и повышает его понятность для простых функций, но как только мы имеем дело со вложенной функцией обратного вызова, лучше давать имена параметрам обеих функций:
// Простая функция
executeInBackgroud {
if (it.wait()) ...
....
}
// вложенная функция
executeRequest { response ->
if (response.body() == null) return
executeDB { dbHelper ->
dbHelper.update(user)
...
}
}
Для тех кто не знает, в файле Standard.kt [2] находится множество полезных функций. Здесь [3] приведено подробное описание для чего нужна каждая из них.
Проблемы с этими функциями начинаются тогда, когда программист начинает их использовать слишком часто.
Первый пример — функция let, которая по сути выполняет 2 задачи: позволяет вызвать код, если какое-то значение не равно null и перекладывает это значение в переменную it:
response?.user?.let {
val name = it.name // в it теперь лежит объект user
}
Первый недостаток данной функции пересекается с темой предыдущего раздела — появляется переменная it, которая добавляет возможных ошибок. Второй недостаток — с использованием этой функции код не читается как английский текст. Намного лучше написать следующим образом:
val user = response?.user ?: return
val name = user.name
В третьих, let добавляет лишний уровень отступа, что ухудшает читаемость кода. Почитать по поводу данной функции можно здесь [4], здесь [5] и здесь [6]. Моё мнение — данная функция вообще не нужна в языке, единственный плюс от нее — помощь с null safety. Однако даже этот плюс можно решить другими более изящными и понятными способами (предварительная проверка на null при помощи ?: или просто if).
Что касается остальных функций, то они должны применятся крайне редко и осторожно. Возьмем, к примеру, with. Она позволяет не указывать каждый раз объект, на котором нужно вызвать функцию:
with(dbHelper) {
update(user)
delete(comment)
}
// вышеприведенный код эквивалентен следующему:
dbHelper.update(user)
dbHelper.delete(comment)
Проблема начинается там, где данные вызовы перемешаны с другим кодом, не относящимся к объекту dbHelper:
with(dbHelper) {
val user = query(user.id)
user.name = name
user.address = getAddress() // getAddress() не относится к объекту dbHelper
....
update(user)
val comment = getLatestComment() // getLatestComment() также не относится к объекту dbHelper
....
delete(comment)
}
В данном случае приходится постоянно следить за тем, кому же на самом деле принадлежит та или иная функция, что значительно снижает читаемость. Пример со вложенным использованием with приводить не буду, и так понятно, какой спагетти-код получится в итоге.
О других наболевших вещах напишу в следующей статье, потому что это уже успела разрастись.
Автор: skatset
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/177624
Ссылки в тексте:
[1] Здесь: https://discuss.kotlinlang.org/t/will-it-variable-stay-or-go/522/
[2] Standard.kt: https://github.com/JetBrains/kotlin/blob/1.0.3/libraries/stdlib/src/kotlin/util/Standard.kt
[3] Здесь: http://beust.com/weblog/2015/10/30/exploring-the-kotlin-standard-library/
[4] здесь: http://beust.com/weblog/2016/01/14/a-close-look-at-kotlins-let/
[5] здесь: https://www.reddit.com/r/Kotlin/comments/41dg3r/a_close_look_at_kotlins_let/
[6] здесь: https://discuss.kotlinlang.org/t/usecases-let-function-t-let-f-t-r/387
[7] Источник: https://habrahabr.ru/post/308312/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.