Что такое companion objects kotlin
Рефакторинг функций расширения в Kotlin: использование объекта-компаньона
В Kotlin есть отличная возможность использовать функции расширения, позволяющие писать более выразительный и компактный код. Под капотом это просто статические методы, которые могут навредить кодовой базе при некорректном использовании. В этой статье я расскажу, как работать с функциями расширения, которые со временем из небольших добавлений к коду трансформировались в монстров, содержащих бизнес-логику.
Точка отсчёта
Допустим, у нас есть такая функция:
Описанный далее подход к рефакторингу можно применять и к функциям верхнего уровня, а также к методам синглтона:
Как видите, функции проверяют, доступна ли биометрия на текущем устройстве. Довольно простая и понятная логика для реализации в виде метода расширения.
Тестируемость
Функции расширения по сути являются @JVMStatic методами конкретного вспомогательного класса. Вот Java-эквивалент этого метода:
Вообще мы используем старомодный синглтон (определённый в области видимости класса с помощью статического модификатора), к которому можем обращаться из любого места кода, чтобы воспользоваться его логикой. А в чём главная проблема синглтонов? В тестируемости.
Рассмотрим такой случай:
Решить эту проблему можно с помощью Robolectric или запуска теста на устройстве. Но нужно ли нам это? Эти варианты тестирования займут намного больше времени.
Усложнение логики
Что ещё может произойти со вспомогательными функциями? Они могут сильно усложниться, а мы поздно это заметим. В контексте предыдущего примера представим, что у нас появилось новое требование: при каждой проверке доступности аппаратной биометрии нужно также проверять результат А/В-теста, чтобы активировать функциональность.
Во-первых, не следует отправлять в продакшен код, который я сейчас покажу (это просто пример). Во-вторых, рано или поздно вы всё равно столкнётесь с проблемой усложнения функций расширения в продакшен-коде. Никто не идеален.
Получилась весьма неприглядная функция расширения, в которой смешаны бизнес-логика и логика данных. Её может быть трудно отрефакторить, если она применяется во многих местах.
Проблема большой кодовой базы
А что мешает нам просто реализовать новый класс для обработки логики и внедрить в конструктор каждого класса, который его использует? Мешает размер пул-реквеста.
В больших кодовых базах функция расширения может использоваться в десятках и даже сотнях разных мест. И для каждого места в коде потребуется вносить изменения в соответствующий конструктор класса. Если у вас многомодульное приложение с прямыми и явными зависимостями, то может потребоваться явно объявить новый класс в виде зависимости в каждом модуле, который его использует.
Из-за всех этих изменений ваш пул-реквест может раздуться до гигантских размеров — и его будет сложно проверить. К тому же возрастёт риск пропустить ошибку.
С помощью описанного ниже подхода мы сможем реализовать каждый этап в виде отдельного пул-реквеста.
Замена функции расширения на синглтон
Сначала признаем проблему использования синглтонов. Нам нужно заменить неявный синглтон на явный:
Волшебство объекта-компаньона интерфейса
Теперь у нас есть класс, с которым можно работать. Поскольку мы стремимся к тестируемости, в будущем мы заменим прямые использования класса BiometricsUtils на интерфейс. Сейчас интерфейс выглядит так:
Вернёмся к варианту с параметрами в методе и в конце дополнительным этапом мигрируем на вариант без них.
Теперь у нас есть интерфейс и синглтон-класс. Как нам их соединить друг с другом, чтобы потом не пришлось вносить изменения во всех случаях использования метода?
Нам поможет объект-компаньон.
К счастью, требование использовать Companion отменили. И теперь мы можем обращаться к объектам-компаньонам в привычной манере — как к статическим функциям Java.
Более того, компилятор Kotlin достаточно сообразителен, чтобы различать вызовы методов интерфейса и его компаньона.
И поскольку объекты-компаньоны в Kotlin — это обычный синглтон-класс, они могут расширять классы и интерфейсы. Это приводит нас к объекту-компаньону, который реализует интерфейс своего родителя:
Внедрение интерфейса в качестве значения по умолчанию
Раз у нас теперь есть интерфейс, мы можем передать его в качестве параметра конструктора.
Убираем значение по умолчанию
Теперь можно убрать значение по умолчанию параметра biometricsUtils и через DI-систему подставить реальное значение.
Улучшаем интерфейс
Добавим новую реализацию BiometricsUtils :
Теперь через DI-систему предоставим новый класс:
Можем убрать все применения старого метода и обновить использовавшие его тесты.
Заключение
Мы смогли аккуратно отрефакторить метод расширения, поэтапно внося изменения. При этом мы не создали помехи коллегам и минимизировали количество конфликтов слияния.
Объект-компаньон интерфейса — это мощная функция, позволяющая использовать синглтоны, которые легко внедрить в конструктор и заменить заглушками.
Анонимные объекты и объявление объектов
Иногда нам необходимо получить экземпляр некоторого класса с незначительной модификацией, желательно без написания нового подкласса. Kotlin справляется с этим с помощью объявления объектов и анонимных объектов.
Анонимные объекты (ориг.: Object expressions)
Создание анонимных объектов с нуля
Наследование анонимных объектов от супертипов
Для того, чтобы создать объект анонимного класса, который наследуется от какого-то типа (типов), укажите этот тип после object и двоеточия ( : ). Затем реализуйте или переопределите члены этого класса, как если бы вы наследовали от него.
Если у супертипа есть конструктор, то в него должны быть переданы соответствующие параметры. Множество супертипов может быть указано после двоеточия в виде списка, заполненного через запятую.
Использование анонимных объектов в качестве возвращаемых типов значений
Когда анонимный объект используется в качестве типа local или private, но не встроенного объявления (функции или свойства), все его члены доступны через эту функцию или свойство.
Если эта функция или свойство маркированы как встроенные и public или private одновременно, то их тип:
Во всех этих случаях члены, добавленные в анонимный объект, недоступны. Переопределенные члены доступны, если они объявлены в фактическом типе функции или свойства.
Доступ к переменным из анонимных объектов
Код внутри объявленного объекта может обращаться к переменным из окружающей области видимости.
Объявления объектов (ориг.: Object declarations)
Инициализация объявления объекта потокобезопасна и выполняется при первом доступе.
Для непосредственной ссылки на объект используется его имя.
Подобные объекты могут иметь супертипы:
Объявление объекта не может иметь локальный характер (т.е. быть вложенным непосредственно в функцию), но может быть вложено в объявление другого объекта или какого-либо невложенного класса.
Вспомогательные объекты
Для вызова членов такого companion объекта используется имя класса.
Члены класса могут получить доступ к private членам соответствующего вспомогательного объекта.
Имя класса, используемого не в качестве определителя другого имени, действует как ссылка на вспомогательный объект класса (независимо от того, именован он или нет).
Хотя такие члены вспомогательных объектов и выглядят, как статические члены в других языках программирования, во время выполнения они являются членами реальных объектов и могут реализовывать, к примеру, интерфейсы.
Семантическое различие между анонимным объектом и декларируемым объектом
Существует только одно смысловое различие между этими двумя понятиями:
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.
Kotlin. Ключевое слово object.
Ключевое слово object позволяет одновременно объявить класс и создать его экземпляр (или другими словами, объект). При этом использовать его можно по-разному:
Объявление объекта
Наверняка вам приходилось хоть раз создавать такой класс, который должен существовать в одном экземпляре. Обычно это реализуется при помощи паттерна проектирования синглтон. В Kotlin же предлагается использовать объявление объекта, которое одновременно сочетает в себе и объявление класса, и создание его единственного экземпляра.
Объекты можно объявлять внутри класса. Такие объекты тоже существуют в единственном экземпляре, т.е. у любого экземпляра класса будет один и тот же экземпляр объекта.
Объект-компаньон (Companion Object)
Как правило объекты-компаньоны используются для объявления переменных и функций, к которым требуется обращаться без создания экземпляра класса. Либо для объявления констант. По сути они своего рода замена статическим членам класса (в отличие от Java, в Kotlin нет статики).
Объект-выражение
При разработке приложений анонимный объект чаще всего используется для реализации обработчика событий (клики по компонентам экрана).
В данном примере создаётся объект, который реализует интерфейс View.OnClickListener и передаётся функции setOnClickListener() в качестве параметра. Этот параметр и является объектом-выражением.
Обратите внимание, что для объекта-выражения не указывается имя. Зачем оно ему, если объект просто передается функции в качестве параметра?
Если же объекту всё таки требуется имя, то его можно сохранить в переменной.
В объекте-выражении можно обращаться к переменным, которые находятся в той же функции, в которой был создан анонимный объект.
Анонимный объект может реализовывать несколько интерфейсов, тогда они перечисляются через запятую. А может и вовсе не реализовывать ни один.
Обратите внимание, что анонимные объекты не являются синглтонами. Каждый раз при выполнении объекта-выражения создаётся новый объект.
Полезные ссылки
Русские Блоги
Сопутствующие объекты и статические члены в Kotlin
Введение
Недавно компания разработала проект с использованием Kotlin, поэтому мне нужно изучить Kotlin, ведь это первый официальный язык разработки Android! При нормальной разработке я привык определять метод запуска Activity в целевой Activity как статический метод, как показано ниже:
Преимущество этого написания состоит в том, что вызывающему абоненту удобно звонить, вызывающий может четко знать, какие параметры нужны целевому Activity, и не заботиться о ключе параметра. Но в Котлине нет static Как бороться с этим ключевым словом? Здесь вам нужно использовать для обработки объекты-компаньоны Kotlin. Итак, эта статья в основном знакомит с применением сопутствующих объектов в Kotlin.
Два, объект-компаньон
1. Заявление:
В Kotlin можно использовать объявление объекта, определенное в классе. companion Украсьте так, чтобы этот объект был сопутствующим объектом. Следующий пример:
2. Позвоните
Использовать companion После изменения объекта ключевым словом сопутствующий объект эквивалентен объекту внешнего класса. Мы можем использовать этот класс для прямого вызова, как показано ниже:
Из приведенного выше кода видно, что имя сопутствующего объекта не имеет ничего общего с вызовом, поэтому имя Obj Его можно не указывать.
3. Роль сопутствующих объектов
4. Сосуществовать с кодом Java.
Зная характеристики сопутствующих объектов, как мы можем сосуществовать с кодом Java? То есть как вызвать объекты-компаньоны Котлина в Java-коде? следующим образом:
5. Использование @JvmField и @JvmStatic
В приведенном выше примере мы знаем, что члены сопутствующих объектов Kotlin могут вызываться в коде Java, аналогично статическим членам в классах Java. Но, похоже, он немного отличается от такового в Java, потому что между именем класса и именем метода / средством установки свойства и именем метода получения есть дополнительное имя объекта или ключевое слово Companion. Как сделать так, чтобы при вызове он выглядел так же, как в Java?
Котлин предоставляет нам @JvmField с участием @JvmStatic Две ноты. @JvmField Используется в атрибутах, @JvmStatic Используется в методе. Такие как:
Таким образом, когда мы вызываем код Java, форма вызова статического члена класса Java остается неизменной, а способ вызова кода Kotlin остается неизменным:
6, ключевое слово const
Если вы не используете измененные константы, нам нужно обратиться к вызываемому сопутствующему объекту, и мы должны вызвать метод получения.
Эффект от использования указанного выше ключевого слова const отличается только способом его вызова в Java и не имеет никакого эффекта в Kotlin.
3. Расширение сопутствующих объектов
Если вы знаете Kotlin, вы должны знать, что объекты могут быть расширены в Kotlin. В Kotlin, если класс содержит сопутствующие объекты, Kotlin позволяет сопутствующим объектам расширять методы и свойства. Таким образом, статические члены расширяются для внешнего класса, в котором расположен сопутствующий объект, и метод доступа остается тем же.
Следуйте приведенному выше Liezi, для указанного выше NumberTest Класс расширяет один minus метод:
1. Способ продления
В этом примере мы видим, что мы можем расширить метод по имени класса, если сопутствующий объект имеет имя, используйте Имя класса. Имя сопутствующего объекта. Имя метода () Чтобы расширить, в противном случае используйте Имя класса. Companion. Имя метода () Приходите и расширяйтесь.
2. Расширенные атрибуты
Точно так же мы можем расширять атрибуты, но расширенные атрибуты имеют следующие характеристики:
Четыре, резюме
Давайте начнем с небольшого эпизода и вернемся к предисловию к этой статье. Метод запуска, определенный в Activity, может быть записан в следующем формате, когда используется сопутствующий объект: