Kotlin предлагает множество полезных концепций и структур, которые облегчают написание лаконичного кода. Но при работе в команде основной целью должно быть написание кода, который легко читать, понимать и поддерживать. Мы рассмотрим несколько эффективных практик, которые позволят сохранить здоровую кодовую базу.
Примечание: это всего лишь рекомендации и не означает, что это правильный путь. Стиль написания кода зависит от ваших предпочтений и предпочтений вашей команды.
1. Обращайте внимание на видимость классов
Обратите пристальное внимание на модификатор видимости, который вы применяете к новым классам и функциям. По умолчанию классы являются публичными, что означает, что класс будет доступен из любого другого модуля, зависящего от этого класса.
Поддерживаемые модификаторы видимости для классов:
- public: модификатор по умолчанию, видимый для всех классов внутри модуля, а также для любого модуля, зависящего от этого модуля
- internal: виден всем классам внутри модуля, но не за его пределами
- private: виден только внутри файла или класса.
Кроме того, для членов класса (функций и свойств) существует модификатор protected, который делает их видимыми для любого класса, расширяющего данный.
Старайтесь по возможности использовать модификатор internal для классов, чтобы ограничить их видимость только внутри текущего модуля. Таким образом, вы уменьшаете внешний API модуля.
Если вы создаете библиотеку или SDK, подумайте о включении режима Explicit API в Kotlin. Вы можете настроить его таким образом, чтобы для каждого нового класса или функции был явно определен модификатор видимости, иначе компилятор выдаст предупреждение или даже выбросит исключение во время сборки.
2. Сведите количество объявлений верхнего уровня к минимуму
Функции верхнего уровня (функции, существующие вне класса) могут быть очень полезны для определения вспомогательных/полезных функций без необходимости объявления класса. Особенно полезными могут быть функции расширения, позволяющие расширить функциональность класса, который нам не принадлежит, без необходимости наследоваться от него или использовать паттерны проектирования.
Однако очень легко злоупотребить функциями верхнего уровня. В качестве примера рассмотрим приведенную ниже функцию расширения. Она определена на верхнем уровне, имеет видимость public и проверяет, является ли String правильным именем пользователя. Это вполне допустимая функция, и она имеет смысл в контексте экрана входа в систему.
// top-level declaration fun String.isValidUsername(): Boolean { return this.matches(Regex("^[a-zA-Z0-9._-]{3,15}\$")) }
Однако, поскольку функция публичная и определена как функция верхнего уровня, это означает, что она будет доступна из любого места внутри модуля, в котором она определена, и в модулях, которые зависят от этого модуля. Если у вас приложение с одним модулем и вы написали эту функцию расширения, то в любой момент, когда вы захотите вызвать функцию для String, в списке предложений появится эта несвязанная функция isValidUsername(). При появлении большого количества функций такого типа опыт разработчика будет ухудшаться, поскольку предложения будут становиться все более нерелевантными.
Попробуйте ограничить область видимости функций расширения тем файлом, классом или модулем, где они имеют контекстный смысл. Мы могли бы изменить модификатор видимости приведенной выше функции на internal или даже private.
Кроме того, функции верхнего уровня обычно труднее обнаружить, а это значит, что новые разработчики, не знающие о них, скорее всего, не будут их использовать. Лучше сгруппировать связанные функции внутри специального класса.
3. Предпочтите читабельность экономии нескольких строк
Kotlin предлагает мощный синтаксис, позволяющий легко выполнять множество действий в одной строке. Однако иногда умничанье делает код более трудным для чтения другими разработчиками. Отдайте предпочтение ясному, простому синтаксису перед сложными цепочками операторов, даже если это потребует нескольких дополнительных строк кода.
Приведем несколько надуманный пример, но он, надеюсь, должен проиллюстрировать суть. У нас есть функция, которая должна возвращать квадрат входного числа, если входное число больше 0. В противном случае она должна возвращать 0. Попытаемся проявить смекалку и написать ее в одной строке, используя несколько операторов.
private fun squareIfPositive(someNumber: Int): Int { return someNumber.takeIf { it > 0 }?.let { it * it } ?: 0 } // or private fun squareIfPositive(someNumber: Int): Int { return someNumber .takeIf { it > 0 } ?.let { it * it } ?: 0 }
А вот та же функция, но написанная более скучным, почти Java-подобным способом.
// option 1 private fun squareIfPositive(someNumber: Int): Int { return if (someNumber > 0) { someNumber * someNumber } else { 0 } } // option 2 private fun squareIfPositive(someNumber: Int): Int { if (someNumber <= 0) { return 0 } return someNumber * someNumber }
Хотя «скучное» решение не так умно, как однострочное, его, вероятно, легче прочитать и понять тому, кто с ним столкнется. Будучи инженерами-программистами, мы тратим больше времени на чтение кода, чем на его написание, поэтому облегчите работу своим коллегам и себе будущему.
4. Предпочтите создание специального класса данных использованию класса Pair или Triple
Встроенные классы Pair и Triple могут быть полезны, когда необходимо вернуть два или три значения из функции. Однако к свойствам этих двух классов не привязан контекст. Это усложняет понимание того, что означает результат функции, возвращающей Pair<String, String>. Возможно, программистам придется прочитать тело функции, чтобы понять, что представляет собой первая String и что — вторая.
Допустим, у нас есть функция для аутентификации пользователя на бэкенде, которая возвращает токены доступа и обновления в виде Pair<String, String>.
suspend fun authenticateUser(username: String): Pair<String, String> { // perform authentication logic return Pair("accessToken", "refreshToken") }
Мы можем улучшить ее, создав новый выделенный класс данных AuthenticationTokens, содержащий явно названные свойства accessToken: String и refreshToken: String. Так будет понятнее, что возвращается и что представляет собой каждое значение.
suspend fun authenticateUser(username: String): AuthenticationTokens { // perform authentication logic return AuthenticationTokens( accessToken = "accessToken", refreshToken = "refreshToken" ) } data class AuthenticationTokens( val accessToken: String, val refreshToken: String )
5. Используйте исчерпывающие операторы when
При использовании оператора when для проверки значения ограниченного класса иерархии, например, класса enum, класса sealed или интерфейса sealed, лучше определить все возможные значения, а не использовать ветвь else как универсальную.
Использование ветви else может привести к потенциальным ошибкам при добавлении нового значения, поскольку разработчик, добавляющий новое значение, должен знать все варианты его использования.
enum class ProfileAnalyticsEvent { PROFILE_OPENED, PROFILE_EDITED, PROFILE_SAVED, PROFILE_DELETED } fun onAnalyticsEvent(event: ProfileAnalyticsEvent) { when (event) { ProfileAnalyticsEvent.PROFILE_OPENED -> trackProfileOpened() ProfileAnalyticsEvent.PROFILE_EDITED -> trackProfileEdited() ProfileAnalyticsEvent.PROFILE_SAVED -> trackProfileSaved() else -> trackProfileDeleted() } }
Допустим, у нас есть возможные события аналитики, определенные в виде перечисления. Эти события отслеживаются в нескольких функциях по всей кодовой базе, а для отслеживания события PROFILE_DELETED мы используем ветку else. К команде присоединяется новый разработчик, которому поручается добавить новое событие PROFILE_CANCELLED. Поскольку он не знаком со всеми местами, где проверяются эти события, он пропускает эту проверку, в результате чего функция trackProfileDeleted() вызывается и для PROFILE_DELETED, и для PROFILE_CANCELLED из-за условия ветви else. Возможно, ошибка будет выявлена в ходе ревью кода, но, возможно, она попадет в продакшн и повлияет на основные метрики.
Этого можно легко избежать, если явно объявить все возможные значения и сделать оператор when исчерпывающим. При добавлении нового значения компилятор сообщит, что оператор when не является исчерпывающим, что позволит нам не упустить ни одного случая использования.
fun onAnalyticsEvent(event: ProfileAnalyticsEvent) { when (event) { ProfileAnalyticsEvent.PROFILE_OPENED -> trackProfileOpened() ProfileAnalyticsEvent.PROFILE_EDITED -> trackProfileEdited() ProfileAnalyticsEvent.PROFILE_SAVED -> trackProfileSaved() ProfileAnalyticsEvent.PROFILE_DELETED -> trackProfileDeleted() ProfileAnalyticsEvent.PROFILE_CANCELLED -> trackProfileCancelled() } }
Заключение
Мы рассмотрели пять советов по улучшению читабельности и сопровождаемости кода. Я надеюсь, что эти советы помогут вам. Сообщите мне о своих мыслях в комментариях и поделитесь советами из своего опыта.