Рефакторинг функций расширения в Kotlin: использование объекта-компаньона
В Kotlin есть отличная возможность использовать функции расширения, позволяющие писать более выразительный и компактный код. Под капотом это просто статические методы, которые могут навредить кодовой базе при некорректном использовании. В этой статье я расскажу, как работать с функциями расширения, которые со временем из небольших добавлений к коду трансформировались в монстров, содержащих бизнес-логику.
Точка отсчёта
Допустим, у нас есть такая функция:
Описанный далее подход к рефакторингу можно применять и к функциям верхнего уровня, а также к методам синглтона:
Как видите, функции проверяют, доступна ли биометрия на текущем устройстве. Довольно простая и понятная логика для реализации в виде метода расширения.
Тестируемость
Функции расширения по сути являются @JVMStatic методами конкретного вспомогательного класса. Вот Java-эквивалент этого метода:
Вообще мы используем старомодный синглтон (определённый в области видимости класса с помощью статического модификатора), к которому можем обращаться из любого места кода, чтобы воспользоваться его логикой. А в чём главная проблема синглтонов? В тестируемости.
Рассмотрим такой случай:
Решить эту проблему можно с помощью Robolectric или запуска теста на устройстве. Но нужно ли нам это? Эти варианты тестирования займут намного больше времени.
Усложнение логики
Что ещё может произойти со вспомогательными функциями? Они могут сильно усложниться, а мы поздно это заметим. В контексте предыдущего примера представим, что у нас появилось новое требование: при каждой проверке доступности аппаратной биометрии нужно также проверять результат А/В-теста, чтобы активировать функциональность.
Во-первых, не следует отправлять в продакшен код, который я сейчас покажу (это просто пример). Во-вторых, рано или поздно вы всё равно столкнётесь с проблемой усложнения функций расширения в продакшен-коде. Никто не идеален.
Получилась весьма неприглядная функция расширения, в которой смешаны бизнес-логика и логика данных. Её может быть трудно отрефакторить, если она применяется во многих местах.
Проблема большой кодовой базы
А что мешает нам просто реализовать новый класс для обработки логики и внедрить в конструктор каждого класса, который его использует? Мешает размер пул-реквеста.
В больших кодовых базах функция расширения может использоваться в десятках и даже сотнях разных мест. И для каждого места в коде потребуется вносить изменения в соответствующий конструктор класса. Если у вас многомодульное приложение с прямыми и явными зависимостями, то может потребоваться явно объявить новый класс в виде зависимости в каждом модуле, который его использует.
Из-за всех этих изменений ваш пул-реквест может раздуться до гигантских размеров — и его будет сложно проверить. К тому же возрастёт риск пропустить ошибку.
С помощью описанного ниже подхода мы сможем реализовать каждый этап в виде отдельного пул-реквеста.
Замена функции расширения на синглтон
Сначала признаем проблему использования синглтонов. Нам нужно заменить неявный синглтон на явный:
Волшебство объекта-компаньона интерфейса
Теперь у нас есть класс, с которым можно работать. Поскольку мы стремимся к тестируемости, в будущем мы заменим прямые использования класса BiometricsUtils на интерфейс. Сейчас интерфейс выглядит так:
Вернёмся к варианту с параметрами в методе и в конце дополнительным этапом мигрируем на вариант без них.
Теперь у нас есть интерфейс и синглтон-класс. Как нам их соединить друг с другом, чтобы потом не пришлось вносить изменения во всех случаях использования метода?
Нам поможет объект-компаньон.
К счастью, требование использовать Companion отменили. И теперь мы можем обращаться к объектам-компаньонам в привычной манере — как к статическим функциям Java.
Более того, компилятор Kotlin достаточно сообразителен, чтобы различать вызовы методов интерфейса и его компаньона.
И поскольку объекты-компаньоны в Kotlin — это обычный синглтон-класс, они могут расширять классы и интерфейсы. Это приводит нас к объекту-компаньону, который реализует интерфейс своего родителя:
Внедрение интерфейса в качестве значения по умолчанию
Раз у нас теперь есть интерфейс, мы можем передать его в качестве параметра конструктора.
Убираем значение по умолчанию
Теперь можно убрать значение по умолчанию параметра biometricsUtils и через DI-систему подставить реальное значение.
Улучшаем интерфейс
Добавим новую реализацию BiometricsUtils :
Теперь через DI-систему предоставим новый класс:
Можем убрать все применения старого метода и обновить использовавшие его тесты.
Заключение
Мы смогли аккуратно отрефакторить метод расширения, поэтапно внося изменения. При этом мы не создали помехи коллегам и минимизировали количество конфликтов слияния.
Объект-компаньон интерфейса — это мощная функция, позволяющая использовать синглтоны, которые легко внедрить в конструктор и заменить заглушками.
Why do we use «companion object» as a kind of replacement for Java static fields in Kotlin?
What is the intended meaning of «companion object»? So far I have been using it just to replace Java’s static when I need it.
I am confused with:
which seems like an unidiomatic way of doing it. What’s the better way?
5 Answers 5
What is the intended meaning of «companion object»? Why is it called «companion»?
First, Kotlin doesn’t use the Java concept of static members because Kotlin has its own concept of object s for describing properties and functions connected with singleton state, and Java static part of a class can be elegantly expressed in terms of singleton: it’s a singleton object that can be called by the class’ name. Hence the naming: it’s an object that comes with a class.
Apart from naming, it is more powerful than Java static members: it can extend classes and interfaces, and you can reference and pass it around just like other objects.
Does it mean that to create multiple static properties, I have to group it together inside companion object block?
Yes, that’s the idiomatic way. Or you can even group them in non-companion objects by their meaning:
To instantly create a singleton instance that is scoped to a class, I often write /*. */ which seems like an unidiomatic way of doing it. What’s the better way?
It depends on what you need in each particular case. Your code suits well for storing state bound to a class which is initialized upon the first call to it.
If you don’t need it to be connected with a class, just use object declaration:
You might also find useful ways of initializing singleton state.
This object is a companion of the instances. IIRC there was lengthy discussion here: upcoming-change-class-objects-rethought
Does it mean that to create multiple static properties, I have to group it together inside companion object block?
Yes. Every «static» property/method needs to be placed inside this companion.
To instantly create a singleton instance that is scoped to a class, I often write
You do not create the singleton instance instantly. It is created when accessing singleton for the first time.
which seems like an unidiomatic way of doing it. What’s the better way?
Just keep in mind that Kotlin offers other stuff to organize your code. There are now alternatives to simple static functions e.g. you could use Top-Level-Functions instead.
Why is it called «companion»?
An object declaration inside a class can be marked with the companion keyword:
Members of the companion object can be called by using simply the class name as the qualifier:
If you only use ‘object’ without ‘companion’, you have to do like this:
In my understanding, ‘companion’ means this object is companion with the outter class.
When the classes/objects with related functionalities belong together, they are like companions of each other. A companion means a partner or an associate, in this case.
Reasons for companionship
Cleaner top-level namespace
When some independent function is intended to be used with some specific class only, instead of defining it as a top-level function, we define it in that particular class. This prevents the pollution of top-level namespace and helps with more relevant auto-completion hints by IDE.
Packaging convenience
It’s convenient to keep the classes/objects together when they are closely related to each other in terms of the functionality they offer to each other. We save the effort of keeping them in different files and tracking the association between them.
Code readability
Just by looking at the companionship, you get to know that this object provides helper functionality to the outer class and may not be used in any other contexts. Because if it was to be used with other classes, it would be a separate top level class or object or function.
Primary purpose of companion object
Problem: companion class
Let’s have a look at the kinds of problems the companion objects solve. We’ll take a simple real world example. Say we have a class User to represent a user in our app:
And an interface for the data access object UserDao to add or remove the User from the database:
Now since the functionalities of the User and implementation of the UserDao are logically related to each other, we may decide to group them together:
While this is a good setup, there are several problems in it:
Solution: companion object
In the User class, we just replace the two words class UserAccess with the two other words companion object and it’s done! All the problems mentioned above have been solved suddenly:
The ability to extend interfaces and classes is one of the features that sets the companion objects apart from Java’s static functionality. Also, companions are objects, we can pass them around to the functions and assign them to variables just like all the other objects in Kotlin. We can pass them to the functions that accept those interfaces and classes and take advantage of the polymorphism.
companion object for compile-time const
This is the kind of grouping you have mentioned in the question. This way we prevent the top-level namespace from polluting with the unrelated constants.
companion object with lazy
The lazy < >construct is not necessary to get a singleton. A companion object is by default a singleton, the object is initialized only once and it is thread safe. It is initialized when its corresponding class is loaded. Use lazy < >when you want to defer the initialization of the member of the companion object or when you have multiple members that you want to be initialized only on their first use, one by one:
In this code, fetching the list and settings are costly operations. So, we use lazy < >construct to initialize them only when they are actually required and first called, not all at once.
The fetching statements will be executed only on the first use.
companion object for factory functions
Also notice that companion objects can have properties ( currentId in this case) to represent state.
companion object extension
Companion objects cannot be inherited but we can use extension functions to enhance their functionality:
This is useful for extending the functionality of the companion objects of third party library classes. Another advantage over Java’s static members.
When to avoid companion object
Somewhat related members
Maintain single responsibility principle
That’s it! Hope that helps make your code more idiomatic to Kotlin.
Kotlin, компиляция в байткод и производительность (часть 2)
Это продолжение публикации. Первую часть можно посмотреть тут
Содержание:
Циклы:
В языке Kotlin отсутствует классический for с тремя частями, как в Java. Кому-то это может показаться проблемой, но если подробнее посмотреть все случаи использования такого цикла, то можно увидеть, что по большей части он применяется как раз для перебора значений. На смену ему в Kotlin есть упрощенная конструкция.
1..10 тут это диапазон по которому происходит итерация. Компилятор Kotlin достаточно умный, он понимает что мы собираемся в данном случае делать и поэтому убирает весь лишний оверхед. Код компилируется в обычный цикл while с переменной счетчика цикла. Никаких итераторов, никакого оверхеда, все достаточно компактно.
Похожий цикл по массиву (который в Kotlin записывается в виде Array ), компилируется аналогичным образом в цикл for.
Немного другая ситуация возникает, когда происходит перебор элементов из списка:
В этом случае приходится использовать итератор:
Таким образом, в зависимости от того по каким элементам происходит перебор, компилятор Kotlin сам выбирает самый эффективный способ преобразовать цикл в байткод.
Ниже приведено сравнение производительности для циклов с аналогичными решениями в Java:
Циклы
Как видно разница между Kotlin и Java минимальна. Байткод получается очень близким к тому что генерирует javac. По словам разработчиков они еще планируют улучшить это в следующих версиях Kotlin, чтобы результирующий байткод был максимально близок к тем паттернам, которые генерирует javac.
When — это аналог switch из Java, только с большей функциональностью. Рассмотрим ниже несколько примеров и то, во что они компилируются:
Для такого простого случая результирующий код компилируется в обычный switch, тут никакой магии не происходит:
Если же немного изменить пример выше, и добавить константы:
То код в этом случае уже компилируется в следующий вид:
Это происходит потому, что на данный момент компилятор Kotlin не понимает, что значения являются константами, и вместо преобразования к switch, код преобразуется к набору сравнений. Поэтому вместо константного времени происходит переход к линейному (в зависимости от количества сравнений). По словам разработчиков языка, в будущем это может быть легко исправлено, но в текущей версии это пока так.
Существует также возможность использовать модификатор const для констант, известных на момент компиляции.
Тогда в этом случае компилятор уже правильно оптимизирует when:
Если же заменить константы на Enum:
То код, также как в первом случае, будет компилироваться в switch (практический такой же как в случае перебора enum в Java).
По ordinal номеру элемента определяется номер ветки в switch, по которому далее и происходит выбор нужной ветви.
Посмотрим на сравнение производительности решений на Kotlin и Java:
Как видно простой switch работает точно также. В случае, когда компилятор Kotlin не смог определить что переменные константы и перешел к сравнениям, Java работает чуть быстрее. И в ситуации, когда перебираем значения enum, также есть небольшая потеря на возню с определением ветви по значению ordinal. Но все эти недостатки будут исправлены в будущих версиях, и к тому же потеря в производительности не очень большая, а в критичных местах можно переписать код на другой вариант. Вполне разумная цена за удобство использования.
Делегаты
Делегирование — это хорошая альтернатива наследованию, и Kotlin поддерживает его прямо из коробки. Рассмотрим простой пример с делегированием класса:
Класс Derived в конструкторе получает экземпляр класса, реализующий интерфейс Base, и в свою очередь делегирует реализацию всех методов интерфейса Base к передаваемому экземпляру. Декомпилированный код класса Derived будет выглядеть следующим образом:
В конструктор класса передается экземпляр класса, который запоминается в неизменяемом внутреннем поле. Также переопределяется метод print интерфейса Base, в котором просто происходит вызов метода из делегата. Все достаточно просто.
Существует также возможность делегировать не только реализацию всего класса, но и отдельных его свойств (а с версии 1.1 еще возможно делегировать инициализацию в локальных переменных).
Компилируется в код:
При инициализации класса DeleteExample создается экземпляр класса Delegate, сохраняемый в поле name$delegate. И далее вызов функции getName переадресовывается к вызову функции getValue из name$delegate.
В Kotlin есть уже несколько стандартных делегатов:
— lazy, для ленивых вычислений значения поля.
— observable, который позволяет получать уведомления обо всех изменения значения поля
— map, используемый для инициализации значений поля из значений Map.
Object и companion object
В Kotlin нет модификатора static для методов и полей. Вместо них, по большей части, рекомендуется использовать функции на уровне файла. Если же нужно объявить функции, которые можно вызывать без экземпляра класса, то для этого есть object и companion object. Рассмотрим на примерах как они выглядят в байткоде:
Простое объявление object с одним методом выглядит следующим образом:
В коде дальше можно обращаться к методу objectFun без создания экземпляра ObjectExample. Код компилируется в практически каноничный синглтон:
Компилируется к вызову INSTANCE:
companion object используется для создания аналогичных методов только уже в классе, для которого предполагается создание экземпляров.
Обращение к методу companionFun также не требует создания экземпляра класса, и в Kotlin будет выглядеть как простое обращение к статическому методу. Но на самом деле происходит обращение к компаньону класса. Посмотрим декомпилированный код:
Компилятор Kotlin упрощает вызовы, но из Java, правда, выглядит уже не так красиво. К счастью, есть возможность объявить методы по настоящему статическими. Для этого существует аннотация @JvmStatic. Ее можно добавить как к методам object, так и к методам companion object. Рассмотрим на примере object:
В этом случае метод staticFun будет действительно объявлен статическим:
Для методов из companion object тоже можно добавить аннотацию @JvmStatic:
Для такого кода будет также создан статичный метод companionFun. Но сам метод все равно будет вызывать метод из компаньона:
Как показано выше, Kotlin предоставляет различные возможности для объявления как статических методов так и методов компаньонов. Вызов статических методов чуть быстрее, поэтому в местах, где важна производительность, все же лучше ставить аннотации @JvmStatic на методы (но все равно не стоит рассчитывать на большой выигрыш в быстродействии)
lateinit свойства
Иногда возникает ситуация, когда нужно объявить notnull свойство в классе, значение для которого мы не можем сразу указать. Но при инициализации notnull поля мы обязаны присвоить ему значение по умолчанию, либо сделать свойство Nullable и записать в него null. Чтобы не переходить к nullable, в Kotlin существует специальный модификатор lateinit, который говорит компилятору Kotlin о том, что мы обязуемся сами позднее инициализировать свойство.
Если же мы попробуем обратиться к свойству без инициализации, то будет брошено исключение UninitializedPropertyAccessException. Подобная функциональность работает достаточно просто:
В getter вставляется дополнительная проверка значения свойства, и если в нем хранится null, то кидается исключение. Кстати именно из-за этого в Kotlin нельзя сделать lateinit свойство с типом Int, Long и других типов, которые соответствуют примитивным типам Java.
coroutines
В версии Kotlin 1.1 появилась новая функциональность, называемая корутины (coroutines). С ее помощью можно легко писать асинхронный код в синхронном виде. Помимо основной библиотеки (kotlinx-coroutines-core) для поддержки прерываний, есть еще и большой набор библиотек с различными расширениями:
kotlinx-coroutines-jdk8 — дополнительная библиотека для JDK8
kotlinx-coroutines-nio — расширения для асинхронного IO из JDK7+.
kotlinx-coroutines-reactive — утилиты для реактивных стримов
kotlinx-coroutines-reactor — утилиты для Reactor
kotlinx-coroutines-rx1 — утилиты для RxJava 1.x
kotlinx-coroutines-rx2 — утилиты для RxJava 2.x
kotlinx-coroutines-android — UI контекст для Android.
kotlinx-coroutines-javafx — JavaFx контекст для JavaFX UI приложений.
kotlinx-coroutines-swing — Swing контекст для Swing UI приложений.
Примечание: Функциональность пока находится в экспериментальной стадии, поэтому все сказанное ниже еще может измениться.
Для того, чтобы обозначить, что функция может быть прервана и использована в контексте прерывания, используется модификатор suspend
Декомпилированный код выглядит следующим образом:
Получается практически исходная функция, за исключением того, что еще передается один дополнительный параметр, реализующий интерфейс Continuation.
В нем хранится контекст выполнения, определена функция возвращения результата и функция возвращения исключения, в случае ошибки.
Корутины компилируются в конечный автомат (state machine). Рассмотрим на примере:
Функции foo и bar возвращают CompletableFuture, на которых вызывается suspend функция await. Декомпилировать в Java такой код не получится (по большей части из-за goto), поэтому рассмотрим его в псевдокоде:
Сами корутины могут выполняться в различных потоках, есть удобный механизм для управления этим при помощи указания пула в контексте запуска корутины. Можно посмотреть подробный гайд с большим количеством примеров и описанием их использования.
Все исходные коды на Kotlin доступны в github. Можно открыть их у себя и поэкспериментировать с кодом, параллельно просматривая, в какой итоговый байткод компилируются исходники.
Выводы
Производительность приложений на Kotlin будет не сильно хуже, чем на Java, а с использованием модификатора inline может даже оказаться лучше. Компилятор во всех местах старается генерировать наиболее оптимизированный байткод. Поэтому не стоит бояться, что при переходе на Kotlin вы получите большое ухудшение производительности. А в особо критичных местах, зная во что компилируется Kotlin, всегда можно переписать код на более подходящий вариант. Небольшая плата за то, что язык позволяет реализовывать сложные конструкции в достаточно лаконичном и простом виде.
Спасибо за внимание! Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточности написать мне об этом в личном сообщении.
Object expressions and declarations
Sometimes you need to create an object that is a slight modification of some class, without explicitly declaring a new subclass for it. Kotlin can handle this with object expressions and object declarations.
Object expressions
Object expressions create objects of anonymous classes, that is, classes that aren’t explicitly declared with the class declaration. Such classes are useful for one-time use. You can define them from scratch, inherit from existing classes, or implement interfaces. Instances of anonymous classes are also called anonymous objects because they are defined by an expression, not a name.
Creating anonymous objects from scratch
Object expressions start with the object keyword.
If you just need an object that doesn’t have any nontrivial supertypes, write its members in curly braces after object :
Inheriting anonymous objects from supertypes
To create an object of an anonymous class that inherits from some type (or types), specify this type after object and a colon ( : ). Then implement or override the members of this class as if you were inheriting from it:
If a supertype has a constructor, pass appropriate constructor parameters to it. Multiple supertypes can be specified as a comma-delimited list after the colon:
Using anonymous objects as return and value types
When an anonymous object is used as a type of a local or private but not inline declaration (function or property), all its members are accessible via this function or property:
If this function or property is public or private inline, its actual type is:
Any if the anonymous object doesn’t have a declared supertype
The declared supertype of the anonymous object, if there is exactly one such type
The explicitly declared type if there is more than one declared supertype
In all these cases, members added in the anonymous object are not accessible. Overridden members are accessible if they are declared in the actual type of the function or property:
Accessing variables from anonymous objects
The code in object expressions can access variables from the enclosing scope:
Object declarations
The Singleton pattern can be useful in several cases, and Kotlin makes it easy to declare singletons:
This is called an object declaration, and it always has a name following the object keyword. Just like a variable declaration, an object declaration is not an expression, and it cannot be used on the right-hand side of an assignment statement.
The initialization of an object declaration is thread-safe and done on first access.
To refer to the object, use its name directly:
Such objects can have supertypes:
Object declarations can’t be local (that is, they can’t be nested directly inside a function), but they can be nested into other object declarations or non-inner classes.
Companion objects
An object declaration inside a class can be marked with the companion keyword:
Members of the companion object can be called simply by using the class name as the qualifier:
The name of the companion object can be omitted, in which case the name Companion will be used:
Class members can access the private members of the corresponding companion object.
The name of a class used by itself (not as a qualifier to another name) acts as a reference to the companion object of the class (whether named or not):
Note that even though the members of companion objects look like static members in other languages, at runtime those are still instance members of real objects, and can, for example, implement interfaces:
However, on the JVM you can have members of companion objects generated as real static methods and fields if you use the @JvmStatic annotation. See the Java interoperability section for more detail.
Semantic difference between object expressions and declarations
There is one important semantic difference between object expressions and object declarations:
Object expressions are executed (and initialized) immediately, where they are used.
Object declarations are initialized lazily, when accessed for the first time.
A companion object is initialized when the corresponding class is loaded (resolved) that matches the semantics of a Java static initializer.




