Jedeme v Kotlinu!

 

Proč byste si měli přečíst následující články? Protože u nás se Kotlin osvědčil.

Nerozhodujeme se, jestli Java nebo Kotlin, nové věci píšeme v Kotlinu.

Proto se chceme podělit o pár zkušeností.

To, proč je Kotlin tak návykový, je směsicí několika věcí

 

V Artinu rádi prozkoumáváme nové věci a Kotlin nebyl výjimkou. Dostali jsme se k němu před pár lety a od té doby se (nejen u nás) lavinovitě rozšířil. U nových projektů neřešíme otázku, jestli Java nebo Kotlin, odpověď je jasná: Kotlin. U starších projektů zase hledáme cestu, jak alespoň nové featury psát v Kotlinu. Bývá pak těžké odolat pokušení přepsat tu a tam nějaký starší kód z Javy do Kotlinu, zvlášť když stačí v IDEI zmáčknout tu správnou klávesovou zkratku, výsledek trošku poupravit a soubor je najednou poloviční a lépe čitelný.

Proč je Kotlin tak návykový, je směsicí několika věcí. Co však jde z Kotlinu cítit ze všech stran, je pragmatičnost. Řeší věci, které programátory opravdu trápí, a nezabředává do akademických problémů (jako třeba Scala).

Jedna z mých oblíbených funkcionalit v Kotlinu jsou operace nad kolekcemi. Tuto oblast pokrývá několik knihoven. Ty, které jsme aktivně používali, zde popíšu s jejich výhodami a nevýhodami.

Jako jednoduchý (ale v praxi nepoužitelný) příklad pro srovnání operací nad kolekcemi může posloužit konstrukt, který má na vstupu List s čísly a jehož výsledkem je jiný List, ve kterém jsou jen původní čísla menší než 5 a poté vynásobená dvěma.

 

Groovy

Jako jedny z prvních se operace nad kolekcemi objevily v Groovy, které jsme ale používali pouze v menších projektech, testech nebo skriptech. Do větších projektů jsme se s Groovy moc nepouštěli.

V Groovy by náš příklad mohl vypadat třeba takto:

def input = [1, 2, 3, 4, 5, 6, 7, 8, 9] // ArrayList
def output = input
    .findAll { it < 5 }  // nový ArrayList[1, 2, 3, 4]
    .collect { it * 2 }  // nový ArrayList[2, 4, 6, 8]

 

I když to z kódu není na první pohled patrné, Groovy funguje tak, že při každé operaci vytváří nový ArrayList. Ve většině případů to není problém, ale pokud byste se tomu chtěli vyhnout, nenabízí Groovy žádnou alternativu.

Groovy nabízí velmi kompaktní zápis a základní sadu funkcí pro filtrování, transformování a spoustu dalších, ale při delším používání vám některé začnou chybět.

 

Guava FluentIterable

Pak přišla knihovna Guava, která nám vystačila na dlouhou dobu se svým FluentIterable, Function a Predicate. Před příchodem lambd v Java 8 byla Guava hodně ukecaná, s jejich zavedením je zápis kompaktnější.

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> output =
FluentIterable.from(input)     // nové FluentIterable
   .filter(it -> it < 5)       // nové FluentIterable
   .transform(it -> it * 2)    // nové FluentIterable
   .toList();                  // nový ArrayList

 

FluentIterable nabízí sadu funkcí, pro jejich použití je však potřeba vždy FluentIterable vytvořit pomocí některé z metod FluentIterable.from() nebo FluentIterable.of(), což zápis prodlužuje. Pro získání výsledku např. v podobě List je třeba zavolat metodu toList(). V API FluentIterable taky chybí některá funkcionalita a není čistý způsob, jak ji tam přidat.

FluentIterable v Guavě funguje z pohledu vytváření nových objektů přesně naopak než Groovy. Metody filter() a transform() vracejí vždy jen nové FluentIterable, které je view nad tím předchozím. Celý chain se provede a nový List se vytvoří až se zavoláním toList(). Tento „streamovací“ styl jde kombinovat se „zhmotňováním“ výsledků, kde je to potřeba, ale API na to není připraveno a v praxi tento přístup není moc čitelný.

 

Java Streams

S příchodem Streams v Java 8 jsme doufali, že všechny problémy Guavy budou vyřešeny a budeme mít lepší funkcionalitu přímo v JDK. Po prvotním experimentování jsme ale brzy narazili a znovu se vrátili ke Guavě, která s pomocí lambd nabrala nový dech.

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> output = input.stream()      // nový Stream
    .filter(it -> it < 5)                  // nový Stream
    .map(it -> it * 2)                     // nový Stream
    .collect(Collectors.toList());         // nový List

 

Na první pohled vypadá vše uhlazeně a je to hodně podobné zápisu v Guavě výše. První zádrhel je, že Stream se dá použít (obdobně jako Iterator) pouze jednou. Když se jednou přečte, nelze ho číst znovu, musí se vytvořit nová instance. Jeho použití je pak velmi omezené jako parametr metody, kdy by tato metoda mohla Stream zavolat jen jednou. Nemohla by se např. nejdřív zeptat, jestli je Stream prázdný, a až pokud by nebyl, zavolat na něj nějakou další metodu.

Streamy taky trpí tím, že se autoři snažili vyřešit všechny problémy, včetně těch, které neexistují. Spousta interní logiky Streamu se např. zabývá optimálním vyhodnocováním Streamu ve více threadech – funkcionalita v praxi jen obtížně představitelná. Tato a další obecná logika pak komplikuje použití v jednoduchých, běžně používaných případech a výsledkem je složitý zápis, vysoká časová náročnost a spousta vytvořených objektů. Guava pak v tomto Streams s přehledem překoná a to ani neměla možnost si přizpůsobit JDK knihovnu jako Streams.

 

Kotlin

Když jsem poprvé pracoval s Kotlinem, překvapilo mě, jak je oblast práce s kolekcemi standardní knihovny dobře a uceleně pokrytá. A jsem z toho nadšený pořád. Pamatuji si jen na pár případů, kdy mi nějaká funkce chyběla, a nevím, jestli už to nebyla moje rozmazlenost.

val input = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
val output = input
    .filter { it < 5 }        // nový List [1, 2, 3, 4]
    .map { it * 2 }           // nový List [2, 4, 6, 8]

 

Na našem krátkém příkladu jde vidět velká podobnost zápisu s Groovy a stejně tak je obdobná funkčnost. V Kotlinu se při každé operaci taky vytváří nový List, ale Kotlin zároveň nabízí interface a funkcionalitu kolem Sequence, což je obdoba Iterable se stejnou myšlenkou jako FluentIterable z Guavy.

val input = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
val output = input.asSequence()     // nová Sequence
    .filter { it < 5 }              // nová Sequence
    .map { it * 2 }                 // nová Sequence
    .toList()                       // nový List [2, 4, 6, 8]

 

Při použití Sequence se při každé operaci vrátí nová Sequence, která obaluje tu předchozí. Zhmotnění výsledku se provede až finální operací, což je v našem případě funkce toList(), která vytvoří nový List s výsledkem. Mezi světem List a Sequence pak jde jednoduše přecházet pomocí funkcí asSequence() a toList() a to i v průběhu podle potřeby.

Další z obrovských výhod a ukázka ucelenosti Kotlinu je možnost přidat další operace nad List nebo Sequence s tím, že jejich použití se pak nijak neliší od těch vestavěných. Lze na to použít extension funkce.

fun Iterable<Int>.filterLessThan(number: Int): ArrayList<Int> {
    return filterTo(ArrayList(), { it < number })
}

 

S nově nadefinovanou extension funkcí pak můžeme napsat:

val input = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9) 
val output = input
 .filterLessThan(5)        // nový List [1, 2, 3, 4]
 .map { it * 2 }           // nový List [2, 4, 6, 8] 

 

Závěr

Na jednoduchých příkladech jsme si ukázali možnosti práce s kolekcemi v Kotlinu a porovnali s jinými jazyky a knihovnami. Kotlin nabízí nadprůměrně obsáhlou a velmi ucelenou knihovnu pro práci s kolekcemi s možností jejího dalšího rozšíření podle potřeby.

Pro nás je to jedna z mnoha věcí, která dělá práci v Kotlinu jednodušší a proč Kotlin preferujeme před Javou a ostatními jazyky v JVM.

Zobrazit více

Pavel Černocký

 Kotlin, Java Senior Competency Leader

Jarda Müller

Kotlin, Java Developer

Java potřebovala vylepšit

Tohle platilo skoro vždycky a vlastně platí o všem. A Java na sobě pracovala. Ve verzi Java 5, tedy roku 2005, se konečně objevily generiky. Ne každému se líbilo řešení, které si Sun vybral, ale i tak to bylo “heuréka!”, konečně. Poprvé Java výrazně zasáhla do jazyka jako takového, do jeho syntaxe a hned to byl nový svěží moderní jazyk.

Jenže to bylo 2005. Po tom se dlouho nic nedělo, Java 7 sice přinesla diamond operátor, ale to je jen takový cukřík. Přitom způsob psaní kódu se měnil. Funkcionální programování bylo na vzestupu, procesory dostaly více jader a multithreading, mluvilo se o immutable strukturách, rychlých event driven serverech, a Java nic. V podstatě až do vydání Java 8 se nestalo nic zajímavého, co by reflektovalo změny v IT světě. Dokonce i C# (the dark side) byl modernější praktičtější jazyk.

 

Vlaštovky frustrace

To nezůstalo bez odezvy. Java svět byl vždycky vynalézavý, a tak se začaly objevovat jazyky, které sice nebyly Java, ale mohly běžet na Java Virtual Machine, protože se překládaly do stejného bytecode jako Java.

 

Groovy

Groovy vyšel roku 2007 a poměrně rychle získal určitou popularitu. Byl to dynamický jazyk jako např. Python nebo Ruby a měl pár velmi povedených vlastností. Např. closures, pár extra metod v běžných třídách jako “collect” a “each” v kolekcích. Psát testy v Groovy bylo jako zjevení zářné budoucnosti.

Nicméně Groovy nikdy neměl a nemohl nahradit Javu. Byl prostě určený pro jiné použití – testy, skriptování…

 

Scala

Scala si získala alespoň zájem každého, kdo se zajímal o funkcionální programování. Do té doby snad nikdo nevěřil, že ty akademicky krásné myšlenky z Haskelu se budou dát použít i v normálním životě. A ejhle.

Ale taky to nikdy nebyl jazyk, který byste bez váhání použili místo Javy.

 

Java 8

No a pak konečně (2014, to je vážně po dlouhé době) vyšla Java 8 s closures a streams. Konečně nějaká inovace, konečně funkcionální programování. Java 8 se vážně povedla, tedy na to co můžete vyždímat z jazyka, který byl designován v roce 1996.

Jenže tou dobou už hodně programátorů zahlédlo jinou cestu. Java je fajn jazyk, ale Java Virtual Machine je vážně skvělá věc, a neomezuje se jen na Javu. Groovy, Scala a ostatní ukázaly, že polyglot programming je proveditelné, je super a na JVM vážně sedí. A někdo se rozhodl, že je čas udělat jazyk, který bude mít právě ten účel, pro který se používá Java. A ten někdo byli JetBrains a jazyk Kotlin (no jo, já vím, i Ceylon to zkoušel, ale to se neujalo).

Cílem nebylo nahradit Javu (to se rozhodne každý sám), ale navrhnout jazyk, který bude prostě “lepší Java”. Do Kotlinu přebrali všechno, co se osvědčilo. Z Javy, Groovy, Scaly, a určitě nejen odsud. Kotlin má krásně vyvedené closures a snadnou migraci z Javy (ala Groovy), hodně myslí na immutability a funkcionální programování (ala Scala), typovou inferenci s podporou pro IDE (ostatně je to JetBrains). Získal si podporu Spring frameworku, Gradle… No a následují už jen samá pozitiva a sociální jistoty.

Takže teď, když já říkám, že dělám “java projekt”, tak tím vlastně myslím, že dělám projekt na JVM. A jazyk ve kterém to budu psát? Kotlin nejspíš, testy možná v Groovy, umím si předslavit i Clojure na specifické problémy. Bude tam Java? Ne. Tam kde bych použil Javu, použiju “lepší Javu” – Kotlin.

Zobrazit více

Kotlin a výsledky operácií

V Effective Java sa píše: Use exceptions only for exceptional conditions. (Použite výnimky iba vo výnimočných prípadoch.) Ďalej vysvetľuje: A well-designed API must not force its clients to use exceptions for ordinary control flow. (Dobre navrhnuté API nesmie nútiť svojich klientov používať výnimky na bežné riadenie toku programu.) Čo je vhodné použiť v prípadoch, ktoré nie sú výnimočné?

Napríklad, prihlásenie používateľa do aplikácie by mohlo zlyhať preto, že používateľ s daným menom neexistuje, alebo preto, že zadané heslo sa nezhoduje s uloženým. Implementácia by mohla byť urobená pomocou výnimiek.

public void authenticate(String username, String password)
        throws UserNotFoundException, NotAuthenticatedException

Ak používateľské meno a heslo pochádzajú z vstupného formulára, ktorý vypĺňa človek, nesprávne napísané meno alebo heslo nie sú výnimočný prípad. Je potrebné zobraziť chybovú hlášku.

try {
    authenticate(username, password);
    showMessage("Authenticated as " + username);
} catch (UserNotFoundException ex) {
    showMessage("User not found");
} catch (NotAuthenticatedException ex) {
    showMessage("Not authenticated");
}

 

API núti klienta zachytávať výnimky na zobrazenie chybovej hlášky. Okrem toho, že to nie je v súlade s dobrými mravmi (s Effective Java), tento návrh robí ďalšiu dekompozíciu zložitou, napr. nie je jednoduché dosiahnuť, aby volanie metódy authenticate, získavanie chybovej hlášky a volanie showMessage boli na rôznych miestach. Použitie výnimiek nie je vhodné.

 

Effective Java navrhuje dve riešenia:

 1. state-testing method (metóda na testovanie stavu)

 2. distinguished return value (zvláštna návratová hodnota)

 

Prihlásenie používateľa by malo byť transakčné, kontrola existencie používateľa a kontrola hesla v samostatnej metóde nedáva zmysel. Jednoduché riešenie je použitie enumu ako návratovej hodnoty.

public enum AuthenticationResult {
    OK, USER_NOT_FOUND, NOT_AUTHENTICATED
}
public AuthenticationResult authenticate(
    String username, String password
)

 

Napríklad, implementovať získavanie hlášky pre používateľa v samostatnej metóde je teraz jednoduché.

public String getAuthenticationMessage(
        AuthenticationResult result
) {
    switch (result) {
        case OK: return "Authenticated"
        case USER_NOT_FOUND: return "User not found"
        case NOT_AUTHENTICATED: return "Not authenticated"
        default: throw new IllegalArgumentException(
                "Unknown result: " + result
        )
}

 

Prvým problémom uvedenej implementácie je, že v prípade úspešného prihlásenia nie je dostupné používateľské meno. Je možné ho pridať ako argument metódy getAuthenticationMessage, ale v prípade, že výsledok bude iný ako OK, argument bude úplne zbytočný. Lepšie by bolo, keby úspešný výsledok prihlásenia obsahoval informácie o prihlásenom používateľovi.

Druhým problémom uvedenej implementácie je, že v prípade, že by vznikol nový možný výsledok prihlásenia (napr. nový chybový stav), metóda getAuthenticationMessage by stále bola kompilovateľná a pri behu programu, ak by nový výsledok prihlásenia nastal, došlo by k vyhodeniu výnimky.

Koncept sealed class v Kotline umožňuje oba problémy vyriešiť. Triedy v Kotline (bez modifikátora) sú final z hľadiska Javy, tj. nie je možné vytvárať potomkov k týmto triedam. Triedy s modifikátorom open môžu mať potomkov. Triedy s modifikátorom sealed môžu mať potomkov, ale všetci potomkovia takej triedy musia byť definovaný v rovnakom súbore ako trieda s modifikátorom sealed.

sealed class AuthenticationResult {
    class Ok(val user: User) : AuthenticationResult()
    object UserNotFound : AuthenticationResult()
    object NotAuthenticated : AuthenticationResult()
}

 

UserNotFound a NotAuthenticated nemusia byť definované ako triedy. Je výhodné využiť kľúčové slovo object, ktoré definuje singleton. Nemá zmysel, aby existovala viac než jedna inštancia UserNotFound a viac než jedna inštancia NotAuthenticated.

Trieda AuthenticationResult.Ok obsahuje pole user. Implementácia metódy authenticate do tohto poľa nastaví prihláseného používateľa.

fun authenticate(
        username: String, password: String
): AuthenticationResult {
    val user = userRepository.getUserByName(username)
    return when {
        user == null -> AuthenticationResult.UserNotFound
        user.passwordHash != password.sha256hash() ->
                AuthenticationResult.NotAuthenticated
        else -> AuthenticationResult.Ok(user)
    }
  }

 

Implementácia getAuthenticationMessage v Kotline s využitím sealed class je podobná, ako v Jave s enumom. V Kotline je navyše možné implementovať ju ako extension method (rozširujúcu metódu) na AuthenticationResult.

fun AuthenticationResult.getAuthenticationMessage(): String =
        when (this) {
            is AuthenticationResult.Ok ->
                    "Authenticated as ${user.name}"
            AuthenticationResult.UserNotFound ->
                    "User not found"
            AuthenticationResult.NotAuthenticated -> 
                    "Not authenticated"
        }

 

Keďže UserNotFound a NotAuthenticated sú singletony odporúča sa nepoužiť is. Pri vyhodnocovaní when nastane volanie equals, nie kontrola typu.

Prvý problém je vyriešený vďaka tomu, že trieda AuthenticationResult.Ok má pole user. Za povšimnutie stojí, že vo vetve is AuthenticationResult.Ok -> nastane pretypovanie this na AuthenticationResult.Ok a vďaka tomu je možné použiť this.user.name. this je implicitné, preto stačí user.name.

Druhý problém je vyriešený vďaka tomu, že kompilátor pozná všetkých potomkov AuthenticationResult. Pri kompilácii when výrazu kontroluje, či sú pokryté všetky vetvy. Ak vznikne nový možný výsledok prihlásenia (tj. nový potomok AuthenticationResult), v metóde getAuthenticationMessage vznikne kompilačná chyba.

Na koncept sealed class sa dá pozerať ako na rozšírenie enumu, vďaka ktorému “enumová hodnota” môže mať parameter. To je možné výhodne využiť pre účely návratových hodnôt rôznych operácií. Uvedený príklad bol triviálny a výhodnosť sealed class sa v tomto kontexte nemusí javiť ako zásadná, ale u veľkých aplikácií je bežné, že obsahujú veľké množstvo operácií, ktoré sa vzájomne volajú a každá z nich môže zlyhať mnohými spôsobmi a takéto zlyhania nie sú výnimočné. V takom prípade sealed class výrazne zlepší čitateľnosť kódu a vedie programátora k implementácii korektného správania aplikácie v bežných chybových stavoch.

Koncept sealed class je dobrou ukážkou toho, ako korektne implementované statické typovanie môže zlepšiť kvalitu kódu a pohodlnosť programovania. Samotná sealed class by však nebola veľkým pomocníkom. To ako spolupracuje s ostatnými konceptami z Kotlinu – singleton object, when výraz, chytré pretypovanie, atď. – z neho robí taký silný nástroj. Možno nie je také dôležité, aké jednotlivé vlastnosti má programovací jazyk, ale akým spôsobom sú tieto vlastnosti prepojené a ako spolupracujú. To je to, čo je v návrhu Kotlinu zvládnuté výborne.

Zobrazit více

Andrej Herich

Kotlin, Java Developer

Tomáš Masník

Kotlin, Java, JavaScript Lead

Co nám do projektu přinesl Kotlin

Představte si takový standardní Java projekt. Spring Boot, MVC, Hibernate, SQL databáze, LDAP, nějaké integrace, JS frontend. Čím by se to tak dalo vylepšit…? 🙂 Co třeba Kotlin? Nechci tu vypisovat seznam všeho, kde nám Kotlin pomohl. Dá se docela rychle dohledat, co všechno je na něm cool. Tak jen vypíchnu, co se mně osobně líbilo na tomhle projektu nejvíc. Aby toho nebylo moc, zaměřím se na to, co jsme použili při psaní testů.

 

Přehlednější názvy testů

Poté co jsem nějakou dobu dělal v JavaScriptu, jsem si uvědomil, jak hezky strukturované testy se tam dají psát. Ne, že by to v Javě nešlo, ale tak elegantní jako v Kotlinu to rozhodně není. Kombinace @Nested tříd a názvů metod s mezerami se toho ale dosáhne velmi elegantně. Takže tento kód:

@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureMockMvc
class BlackListRecordControllerTest {  
    ...  
    @Nested  
    inner class `find by id` {          

        @Test   
        fun `should return one record`() {...}     

        @Test   
        fun `shouldn't be found`() {...}     

        @Test    
        fun `should be unauthorized`() {...}  
    }   

    @Nested  
    inner class `find all records` {...}   

    @Nested  
    inner class `create record` {...}   

    @Nested  
    inner class `update blackListRecord` {...}   

    @Nested  
    inner class `delete blackListRecord` {...}  
    ...
}

 

Pak v IDEI (programuje někdo v něčem jiným..?) vypadá po spuštění takto. Za mě dobrý.

 

 

 

 

 

 

 

„Lepší“ 3rd party knihovny

Taky se vám občas stává, že prostě některá knihovna neumí to, co zrovna potřebujete? (Ve Springu většinou ne, Spring má všechno. Jen to občas najít… :)) Nebo to umí, ale…?

Na to má Kotlin extension functions. Prostě si tu chybějící funkci doimplementujete a hotovo. My jsme třeba chtěli mít v testech controllerů defaultně vyřešený POST request z MockMvc objektu. Aby se dalo jednoduše zavolat:

@Test
fun `should return newly created user`() {  
    mockMvc.jsonPostRequest(API_USER_URL, userToSave)    
        .andExpect(...)
}

 

místo této obludy:

@Test
fun `should return newly created user`() {  
    mockMvc    
        .perform(      
            post(API_USER_URL)        
                .with(csrf())        
                .content(userToSave.toJson())        
                .contentType(MedaType.APPLICATION_JSON)    
        )    
        .andExpect(...)
}

 

Což se dá v Kotlinu udělat takto:

fun MockMvc.jsonPostRequest(url: String, dataObject: Any): ResultActions {  
    return this.perform(    
        MockMvcRequestBuilders.post(url)      
            .with(csrf())      
            .content(dataObject.toJson())      
            .contentType(MediaType.APPLICATION_JSON)  
    )
}

 

Ano, i v Javě se na to dá napsat metoda:

ResultActions jsonPostRequest(MockMvc mockMvc, String url, Object dataObject) {...}

 

Ale musí mít o parametr víc a navíc… jak chcete v Javě na Objectu definovat toJson()?

 

Generování dat

Data classes jsou jedna z mých nejoblíbenějších featur Kotlinu. Nemám rád gettery a settery a zbytečnou ukecanost. Lombok @Data zase podle mě rozhodně není elegantní řešení. V testech dost často potřebujeme hodně podobná data. My to většinou řešíme tak, že vytvoříme jeden objekt a ten pro konkrétní testy trošku upravíme. Pomocí copy(), třeba takto:

val baseRecord = Record(  
    validTo = null,  
    incidentNumber = "INC0x001",  
    annotation = "testing annotation"
)
...
@Test
fun `should update`() {  
    val recordToUpdate = baseRecord.copy(id = 3L)  
    ...
}

 

A co dál…?

Null safety je těžce návyková vlastnost. Jednoduše napsatelné (a čitelné) lambdy. Příjemné operace na kolekcích. when, with, ?:…

No, Javu už nechci vidět.

Zobrazit více

Kotlin: The Fun Parts

Kotlin jsme u nás školili i interně. Na materiály se můžete podívat zde.

 

Nadchli jste se pro Kotlin stejně jako my? Pak se můžete těšit na pokračování!