W poprzednim wpisie o Kotlinie przewinął się temat data class. Dziś przyjrzymy się temu tworowi trochę dokładniej.

Definiowanie i wymagania

By zdefiniować data class (dalej dc) należy do deklaracji klasy dopisać słowo data. Nie jest to słowo kluczowe i można go normalnie używać w np. nazwach pól, zmiennych etc. Poniżej przykład prostej dc:

Listing 1. Prosta data class

data class Person(val firstName: String, val lastName: String, val email: String, var age: Long, var data: Array<Byte>?);

Oczywiście nie może być aż tak prosto. Istnieją pewne zasady:

  • Główny konstruktor musi mieć co najmniej jeden parametr.
  • Wszystkie parametry tego konstruktora muszą być oznaczone jako var albo val.
  • Klasa nie może być abstrakcyjna, otwarta (oznaczona open), zamknięta (oznaczona sealed) ani być klasą wewnętrzną.
  • Klasa nie może rozszerzać innej klasy, ale może implementować interfejsy

Co w zamian?

Jak już mamy takie kod w którym coś musimy, to fajnie by było mieć coś w zamian. I oczywiście dc dają w zamian. Przede wszystkim implementację equals, hashCode i toString. W Javie podobne zachowanie możemy uzyskać stosując bibliotekę lombok (opisane tu i tu). Oczywiście są to, wraz generowanymi getterami u setterami, tylko zabawki, które nie wychodzą poza pewien standard. My chcemy jednak czegoś więcej.

Metoda copy

Kolejną generowaną metodą jest metoda copy. Służy ona, jak sama nazwa wskazuje, do kopiowania obiektów. Nie jest to jednak tylko prymitywne kopiowanie obiektów jeden do jeden. Jako, że Kotlin obsługuje „parametry nazwane” zatem możemy dokonywać zmian w trakcie kopiowania:

Listing 2. Przykład użycia copy

fun main(args: Array<String&gt) {

    val person = Person("Jan", "Kolwaski", "kowalski@jan.pl", 32L, null);

    val person2 = person.copy(email="jan@kowalski.pl");

    println(person)
    println(person2)

}

To jest całkiem przyjemna rzecz.

Metody component

Kolejnymi metodami jakie otrzymujemy wraz z dc są metody component, numerowane od 1. Jedna metoda na każde pole. Tworzone w kolejności deklaracji pól. Mają one sens getterów. To czego mi tu brakuje to metoda components zwracająca jakąś kolekcję pól. Niestety propozycja dodania tej metody została uwalona.
Same metody nie są niczym szczególnym. Ich użycie jest mocno ograniczone, bo dc nie niesie w sobie informacji o tym, że jest dc. To w czym się przydają to dekonstrukcja obiektów.

Dekonstrukcja

Mechanizm dekonstrukcji jest ciekawym podejściem do problemu wyciągania informacji z obiektu. Otóż mając metody component możemy zrobić „pod spodem” coś takiego:

Listing 3. Przykład użycia dekonstrukcji

fun main(args: Array<String&gt) {

    val person = Person("Jan", "Kolwaski", "kowalski@jan.pl", 32L, null);
    val (firstName, email) = person;
    println(firstName)
    println(email)
}

Tworzymy dwie zmienne firstName i email, do których zostaną przypisane wartości z pierwszych dwóch pól klasy. I tu oczywiście pułapka ponieważ nie będzie działać tu mechanizm nazywania parametrów. email wskazuje zatem na lastName. Mechanizm przydatny gdy na przykład chcemy iterować po mapie, ale to temat na osobny wpis.

Podsumowanie

Data class w Kotlinie to ładnie zrobiony value object. Ładnie w tym sensie, że kompilator samodzielnie wygeneruje pewien zestaw standardowych metod. Ich użycie jest bardzo proste. Podejście to pozwala też na jasną separację danych i klas biznesowych co w połączeniu z rozszerzeniami jest całkiem fajnym mechanizmem.
Niestety są też wady pierwszą z nich jest usuwanie informacji, że klasa jest dc w czasie kompilacji. Drugą brak metody components, co utrudnia pisanie generycznych prezenterów.