Właściwości klas (ang. class properties) możemy umownie podzielić na dwie grupy. Jedne po prostu zapisują i odczytują wartość swojego prywatnego pola. A drugie – zupełnie przeciwnie – posiadają dowolnie skomplikowaną logikę w getterach i setterach.
W ramach tych drugich, coraz częściej widzimy powtarzające się schematy (wzorce?) takie jak: lazy initialization, czy nasłuchiwanie zmian danej wartości.
Aby nie pisać tej standardowej logiki za każdym razem od nowa, Kotlin umożliwia zamknięcie jej wewnątrz klasy, i wielokrotne wykorzystanie za pomocą tzw. „właściwości delegowanych”.
Delegowanie Właściwości
Właściwości delegowane (ang. delegated properties) to specjalna konstrukcja języka, dzięki której możemy przekazać (czyt. delegować) wywołania metod get()
i set()
do innego obiektu (tzw. „delegata”). Służy do tego słowo kluczowe by
użyte przy deklaracji właściwości:
class Example { var prop: String by MyDelegate() }
Oficjalna dokumentacja mówi, że taki delegat nie musi implementować żadnego interfejsu, a jedynie spełniać poniższe warunki:
- Musi posiadać metodę
getValue()
- Jeśli właściwość jest zmienna (
var
) to musi również posiadać metodęsetValue()
Przykład:
class MyDelegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { println("Odczyt wartości '${property.name}' obiektu ${thisRef}") return "Geeky Devs" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("Zmiana wartości '${property.name}' obiektu ${thisRef}") println("Nowa wartość: $value") } }
W parametrze thisRef
otrzymujemy obiekt, który zawiera delegowaną właściwość (w naszym wypadku instancja klasy Example
). Parametr property
(typu KProperty) zawiera opis samej właściwości, m.in. jej nazwę. value
jest natomiast wartością przekazaną do settera:
val e = Example() println(e.prop) e.prop = "Geeky Devs Blog"
Wynik:
Odczyt wartości 'prop' obiektu Example@41b25632 Geeky Devs Zmiana wartości 'prop' obiektu Example@41b25632 Nowa wartość: Geeky Devs Blog
Podejście „bez interfejsu” jest czasem wygodne. Umożliwia np. dodanie metod getValue/setValue
za pomocą rozszerzeń. Ale na co dzień ma sporą wadę: sami musimy zgadywać sygnaturę tych metod 😉 Dlatego jednak polecam implementację jednego z dwóch wbudowanych interfejsów:
interface ReadOnlyProperty<in R, out T> { operator fun getValue(thisRef: R, property: KProperty<*>): T } interface ReadWriteProperty<in R, T> { operator fun getValue(thisRef: R, property: KProperty<*>): T operator fun setValue(thisRef: R, property: KProperty<*>, value: T) }
Co się dzieje „pod spodem”?
Tak naprawdę, mechanizm delegacji jest równie prosty, co błyskotliwy. Jak większość rzeczy w Kotlinie 😉
Dla każdej delegowanej właściwości jest tworzony dodatkowy prywatny obiekt – delegat. Następnie metody get()
i set()
wywołują odpowiednio metody getValue()
i setValue()
delegata:
// Kod napisany przez nas: class Example { var prop: String by MyDelegate() } // Kod wygenerowany przez kompilator: class Example { private val prop$delegate = MyDelegate() var prop: String get() = prop$delegate.getValue(this, this::prop) set(value: String) = prop$delegate.setValue(this, this::prop, value) }
Proste i czyste rozwiązanie.
Standardowe Delegaty
Delegaty, w zależności od potrzeby, możemy napisać sami, opakować w klasę czy bibliotekę, i w łatwy wykorzystywać w dowolnym miejscu.
Natomiast w przypadku najczęściej wykorzystywanych wzorców, nie musimy ich nawet pisać, bo są zawarte w bibliotece standardowej Kotlina.
Poniżej kilka przykładów.
Lazy
Funkcja lazy()
umożliwia nam wygodną implementację wzorca „lazy initialization” – czyli odroczenia inicjalizacji właściwości do momentu jej pierwszego użycia. Jest to funkcja przyjmująca jeden parametr – lambdę, która zostanie wywołana tylko raz, przy pierwszym wywołaniu get()
. Potem będzie już tylko zwracany jej wynik:
val lazyProp: String by lazy { println("Some long initialization...") "Done!" } println(lazyProp) println(lazyProp)
Wynik:
Some long initialization... Done! Done!
Observable
Delegates.observable()
umożliwia obserwację zmian danej wartości. Przyjmuje dwa parametry: wartość początkową, i lambdę – handler, który zostanie odpalony po każdej zmianie wartości:
var name: String by Delegates.observable("<empty>") { prop, oldValue, newValue -> println("$oldValue -> $newValue") } name = "John" name = "James"
Wynik:
<empty> -> John John -> James
Vetoable
Delegates.vetoable()
jest bardzo podobne do observable(). Różnica polega na tym, że handler jest wołany przed zmianą wartości, oraz że zyskuje on „prawo weta”. Zwracany Boolean
określa, czy pozwalamy na zmianę wartości właściwości, czy też nie. false
oznacza weto:
var name: String by Delegates.vetoable("<empty>") { prop, oldValue, newValue -> newValue == "James" } println(name) name = "John" println(name) name = "James" println(name)
Wynik:
<empty> <empty> James
Zapisywanie wartości w Mapie
Najciekawszym przykładem z tej listy jest zapisywanie wartości właściwości wewnątrz Mapy. Przydaje się on, kiedy chcemy robić coś „dynamicznego”, jak np. parsowanie JSONa do obiektu DAO, i z powrotem:
class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map } val sampleMap = mapOf("name" to "John", "age" to 33) val user = User(sampleMap) println(user.name) //prints "John" println(user.age) //prints 33
Powyższy kod działa, ponieważ w Kotlinie typ Map
jest rozszerzony o metody getValue()
i setValue()
.
Jeśli dziwi Cię słówko to
, to tłumaczę, że jest to zwykła funkcja rozszerzająca Any.to()
, tylko zapisana w notacji infix.
Podsumowanie
Właściwości delegowane są kolejnym przykładem przyszłościowego myślenia twórców Kotlina. Zamiast dodawać do języka kolejne modyfikatory typu lazy
czy observable
(jak w innych językach), stworzyli mechanizm umożliwiający tworzenie dowolnych „modyfikatorów” w postaci „delegatów”.
Ja osobiście bardzo często korzystam z lazy()
. Chyba nawet zbyt często 😉 A Tobie jak się podoba ten cały koncept? Korzystałeś już z delegacji? Podziel się swoimi przemyśleniami 😀
To wszystko na dziś. Do następnego wpisu!
Bardzo ciekawe. Lazy pozwoliłoby pewnie zrobić Hibernate’a bez dynamicznych proxy w Javie albo czarów z bytecodem.
Ciekawe. O tym nie pomyślałem. Choć wydaje mi się, że Kotlinowe lazy() nie ma aż takiej mocy sprawczej 😉
Ale dzięki Tobie dopisałem rozdział „Co się dzieje „pod spodem”?”, który trochę bardziej to objaśnia.
Dzięki! 😉
Ciekawy wpis. Dzięki!
Dzięki 😉 Staram się 🙂