Kotlin i Delegowanie adekwatności. Czyli Lazy, Observable i wiele więcej…

blog.geekydevs.com 7 lat temu

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 dzięki tzw. „właściwości delegowanych”.

Delegowanie adekwatnoś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 najważniejsze by użyte przy deklaracji adekwatności:

class Example { var prop: String by MyDelegate() }

Oficjalna dokumentacja mówi, iż taki delegat nie musi implementować żadnego interfejsu, a jedynie spełniać poniższe warunki:

  • Musi posiadać metodę getValue()
  • Jeśli adekwatność 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ą adekwatność (w naszym wypadku instancja klasy Example). Parametr property (typu KProperty) zawiera opis samej adekwatnoś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 dzięki 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 adekwatnoś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 choćby 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 adekwatnoś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, iż handler jest wołany przed zmianą wartości, oraz iż zyskuje on „prawo weta”. Zwracany Boolean określa, czy pozwalamy na zmianę wartości adekwatnoś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 adekwatnoś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ę, iż 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 choćby 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!

Idź do oryginalnego materiału