Don’t Reinvent the Wheel, Delegate It! — статья об использовании встроенных в язык Kotlin средств делегирования реализаций классов и свойств.
Один из ключевых постулатов современного ООП-программирования гласит: «Предпочитайте делегирование наследованию». Это значит, что вместо наследования от какого-либо класса лучше включить инстанс этого класса в другой класс и вызывать его методы при вызове одноименных методов этого класса:
1 2 3 4 5 6 7 8 9 10 11 |
class Class1() { fun method1() { ... } fun method1() { ... } } class Class2(firstClass: Class1) { private val class1 = firstClass fun method1() { firstClass.method1() } fun method2() { firstClass.method2() } } |
Зачем это нужно? Чтобы избежать проблем, когда методы класса-родителя вызывают друг друга. Если method1()
вызывает method2()
, то, переопределив второй метод, мы сломаем работу первого. Делегирование решает эту проблему, так как Class1
и Class2
остаются не связанными друг с другом.
С другой стороны, делегирование усложняет код, и поэтому в Kotlin есть специальное ключевое слово by
, сильно упрощающее жизнь разработчику. Благодаря ему реализовать второй класс можно с помощью всего одной строки:
1 |
class Class2(firstClass: Class1) : Class1 by firstClass |
Это действительно все. Компилятор Kotlin автоматически преобразует эту строку в аналог реализации Class2
из первого примера.
Kotlin также позволяет делегировать реализацию свойств. В следующей записи используется стандартный делегат lazy
, инициализирующий переменную при первом обращении к ней:
1 |
val orm by lazy { KotlinORM("main.db") } |
Еще более интересно работает делегат map
, позволяющий магическим образом взять значения из хешмапа.
1 2 3 4 5 6 7 8 9 10 11 12 |
class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map } val user = User(mapOf( "name" to "John Doe", "age" to 25 )) println(user.name) // "John Doe" println(user.age) // 25 |
Кроме lazy
и map
, в стандартную библиотеку Kotlin включены еще три стандартных делегата:
- nutNull — аналог ключевого слова
lateinit
с более широкими возможностями; - observable — позволяет выполнить код в момент чтения или записи переменной;
- vetoable — похож на observable, но срабатывает перед записью нового значения и может запретить изменение.
Ну и конечно же, любой разработчик может создать собственный делегат. Это всего лишь класс c реализацией операторов getValue()
и setValue()
:
1 2 3 4 5 6 7 8 9 |
class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") } } |
Андрей Бреслав, ключевой разработчик Kotlin, не раз заявлял, что не стал бы реализовывать делегирование классов, будь у него возможность вернуться в прошлое. Эта функция оказалась хоть и полезной, но слишком комплексной в реализации и конфликтующей с новыми возможностями языка Java.