Skoro wiemy już, że Kotlin pozwala na delegowanie właściwości tak, by było one leniwie wartościowane, to zastanówmy się jakie inne możliwości daje nam ten mechanizm.

Obiekt jako mapa

Pomyślmy o obiekcie jako o prostej tablicy asocjacyjnej albo mapie. Kluczem niech będzie String, a wartości to dowolne obiekty. Tak mniej więcej można rozumieć obiekty w JavaScripcie. Podobne działanie ma też metoda \_\_dict\_\_ w pythonie. Jednak najlepszym przykładem tego typu zapisu jest JSON.

Poza JSONem często tego typu podejście stosuje się przy parsowaniu CSV:

  • Wczytaj pierwszy rekord – utwórz listę nazw pól z listy nazw kolumn
  • Dla każdego kolejnego wiersza utwórz mapę nazwa pola → wartość
  • Przekaż mapę do fabryki, która zamieni ją na obiekt.

Powyższy opis jest bardzo uproszczony, ale wszystkie rozwiązania „szybkiego” parsowania CSV korzystają z takiego algorytmu. Oczywiście z modyfikacjami, lecz ogólna zasada zawsze jest taka sama.

Delegowanie do mapy

Wykorzystanie map do opisu zawartości obiektu jest na tyle częstym zjawiskiem, że twórcy języka postanowili dodać mechanizm delegowania właściwości do map. Działa on podobnie jak zwykłe delegaty, z tą różnicą, że mapa, którą wykorzystujemy, musi być parametrem konstruktora:

Listing 1. Delegacja do mapy

class User(val map: Map<String, Any?>) {

    val name: String    by map
    val age: Int         by map

    override fun toString(): String {
        return "User(name=$name; age=$age)"
    }
}

fun main(args: Array<String>) {
    val user = User(mapOf(
            "age" to 10,
            "name" to "Kazio"
    ))

    println(user)
}

Niestety mechanizm ten nie jest w pełni generyczny. Oznacza to, że jeżeli chcemy zagnieździć obiekty, to musimy ręcznie wybierać z mapy interesujące nas klucze:

Listing 2. Delegacja do mapy z obiektem zagnieżdżonym

class Email(val map: Map<String, Any?>) {

    val address: String by map

    override fun toString(): String {
        return "Email(address=$address)"
    }
}


class User(val map: Map<String, Any?>) {

    val name: String    by map
    val age: Int        by map
    val email: Email = Email(map.get("email") as Map<String, Any?>)

    override fun toString(): String {
        return "User(name=$name; age=$age; email=$email)"
    }
}


fun main(args: Array<String>) {
    val user = User(mapOf(
            "age" to 10,
            "name" to "Kazio",
            "email" to mapOf(
                    "address" to "Kazio@kazio.pl"
            )
    ))

    println(user)
}

Jak widać, w przypadku pola email musimy ręcznie określić, na podstawie czego jest ono tworzone.

Na koniec jeszcze jedna uwaga. Jeżeli chcemy używać pól zmiennych (var) to, zamiast niezmiennej mapy musimy użyć mapy mutującej.

Podsumowanie

Mechanizm delegowania do mapy ma pewne mankamenty, ale i tak w znaczącym stopniu upraszcza tworzenie obiektów. Poza wspomnianym problemem z bardziej złożonymi obiektami niektórzy mogą odczuwać poważny dyskomfort z powodu narzuconego wyglądu konstruktora domyślnego. Jednak coś za coś.