Artur Czopek
Artur Czopek Full-Stack Java Developer @ simplecoding.pl

Kotlin w web developerce

Dlaczego warto go spróbować, pod jakim względem jest lepszy od Javy, a gdzie jeszcze sprawia problemy.
17.04.201813 min
Kotlin w web developerce

Kotlin kojarzy się zazwyczaj z jedną z dwóch rzeczy. Zwykły śmiertelnik pomyśli o ketchupie, a programiście przyjdzie do głowy Android - i oba skojarzenia są trafne. Kotlin to język programowania z powodzeniem używany do tworzenia aplikacji na platformę Android. Powinien jednak kojarzyć się przede wszystkim z tym, że jest świetnym językiem do tworzenia jakichkolwiek aplikacji, nie tylko mobilnych. Oceny ketchupu o tej nazwie się nie podejmę.


O Kotlinie słów kilka

Kotlin to język programowania stworzony w 2011 roku i wciąż rozwijany przez firmę JetBrains (to goście od IntelliJ i innych popularnych IDE). Jest dostępny na licencji Apache 2.

Po otworzeniu “Kotlin Language Documentation” i przeskoczeniu spisu treści możemy przeczytać:

Kotlin is a great fit for developing server-side applications, allowing to write concise and expressive code while maintaining full compatibility with existing Java-based technology stacks and a smooth learning curve.

Czyli w skrócie - jest to język stworzony z myślą o serwerowej (back-endowej) części aplikacji. Pozwala pisać ekspresyjny kod i jest kompatybilny z technologiami bazującymi na Javie. Wejście w świat Kotlina jest stosunkowo łatwe.


Dlaczego zdecydowałem się na Kotlina?

Współpraca z Javą

Nie ukrywam - patrząc na niektóre języki programowania i porównując je do Javy dochodzę do wniosku, że pisanie w Javie może być bolesne. Owszem, napiszę w niej wszystko. Owszem, wiele bibliotek i aplikacji jest napisanych w tym języku. Owszem, też mam parę projektów napisanych w Javie. Czy jednak w tych projektach jestem ograniczony tylko i wyłącznie do tego języka?

Nie! Jedną z większych, często wymienianych przez twórców zalet Kotlina jest łatwa współpraca z Javą. Nie ma problemu z dodaniem Kotlina do istniejącego projektu bazującego na Javie, a użycie go jest banalnie proste. Nie musimy przepisywać wszystkiego - możemy zacząć od stworzenia lub przepisania jednej klasy. Użycie go w istniejącym kodzie javowym jest tak samo możliwe i proste, jak w przypadku Javy.


Łatwy w użyciu

Nowy język to pewnie nowe biblioteki do ściągnięcia, kompilatory, bla, bla, bla… nie tym razem! Jeżeli masz odpowiednio skonfigurowane środowisko javowe na swojej maszynie i posiadasz IntelliJ, wystarczy odpowiedni plugin, that’s all. Jeżeli natomiast chcesz zejść niżej, jedną z opcji (od marca 2018 roku) jest pobranie gotowego snapa na Linuxa.

Możliwość kompilacji do Javy 6

Wszyscy jesteśmy zafascynowani nowymi technologiami ale niestety, nie zawsze możemy lub powinniśmy ich używać. W przypadku Kotlina jest inaczej - jesteśmy w stanie nadal korzystać z jego możliwości, pisząc czysty kod, a następnie kompilować go do bytecode'u Javy 6! Potężna funkcjonalność dla projektów z ograniczeniami w zakresie technologii (a takich niestety nie brakuje).

Czytelniejszy kod

Czytelniejszy, a także wymagający małej ilości znaków - by działał tak samo jak w Javie.  

Przykładowo, definicja prostej klasy z dwoma niemutowalnymi polami z getterami, setterami, equalsami, etc. w Kotlinie może wyglądać następująco:

data class Person(val firstName: String, val lastName: String)

Bardziej zaawansowane przykłady później.

Rozbudowane community

Mogłoby się wydawać, że język, który dopiero zaczął zdobywać dużą popularność może wiązać się z wieloma nierozwiązanymi jeszcze problemami, a pomocy nie ma gdzie szukać. Błąd! Społeczność jest spora i jestem pewien, że będzie rosła w najbliższych latach.

Spójrzmy na parę statystyk (stan z dnia 23 marca 2018):

Repozytorium na GitHubie:


Pytania na Stack Overflow:


Kotlin doczekał się również swojej konferencji - KotlinConf. Pierwsza edycja odbywała się w Stanach (prezentacje dostępne na YouTubie). W tym roku będzie miała miejsce w Amsterdamie (psst, CFP trwa do 20 kwietnia!).

Wsparcie wielkich firm

Firma JetBrains małą firmą nie jest. Tym bardziej Google, który ogłosił na ostatnim Google I/O, że Kotlin jest już oficjalnym językiem wspieranym na Androida. Apple również wykazuje zainteresowanie Kotlinem - mimo posiadania podobnego rozwiązania, jakim jest język Swift.

Duża liczba bibliotek

Każdy język ma pewien zestaw napisanych w nim i bardzo często używanych bibliotek. Tu nie jest inaczej. Mamy już między innymi biblioteki do tworzenia aplikacji webowych (Ktor), wstrzykiwania zależności (Kodein), mockowania (Mockk), obsługi JSON-ów (Kotson), programowania typowo funkcyjnego (Arrow) i wiele, wiele innych. Warto też wspomnieć, że Kotlin jest oficjalnie wspierany od Sprinta 5!


Czy Kotlin się sprawdził?

Zdecydowanie!

Nie napisałem żadnej aplikacji mobilnej w Kotlinie, stworzyłem i rozwijałem natomiast kilka aplikacji webowych - w tym dwie autorskie. Jedna z nich była tworzona od zera, a druga (e-Arbiter) dostała Kotlina w trakcie. Co ciekawe, korzystaliśmy tam z podejścia mikroserwisowego, co mogło powodować kolejne problemy. Do tej pierwszej dostępu już nie mam. Natomiast rozwój e-Arbitra zakończył się powodzeniem! Jak wygląda stosunek kodu napisanego w Kotlinie do kodu napisanego Javie?

38.6% : 19.7%

Dwa razy więcej kodu w Kotlinie, niż w Javie! Ale to nie wszystko. Posiadaliśmy jeszcze dodatkową bibliotekę napisaną przez nas, wdrożoną w późniejszym etapie rozwijania aplikacji. Jak tam wygląda ta sama statystyka?

Zero Javy, tylko Kotlin!

Czy podjąłbym taką samą decyzję dzisiaj? Zdecydowanie. Programowanie w Kotlinie daje jeszcze więcej frajdy, kod dosłownie się czyta, a ilość czasu zaoszczędzonego (public final static String, bla, bla, bla… ) jest w tym przypadku nieoceniona. Ale na to też składało się wiele innych czynników.


Dlaczego Kotlin jest lepszy od Javy? Konkretne przypadki

Boilerplate’owi mówimy bye, bye!

Boilerplate i Java. Pojęcia nierozłączne, niestety. Często trzeba naskrobać dużo słówek kluczowych, a nazwy zmiennych i funkcji są długie - bo wzorce, bo inaczej ryzyko… w każdym bądź razie - bolesne. Już wyżej pokazałem, o ile prościej można zdefiniować klasę w Kotlinie. A może jakaś funkcja? Czemu nie!

Kotlin:

fun getAllUserNamesAndEmails() = userRepository.findAll().map { UserNameEmail(it.name, it.email) }


Java (tylko 8 i wyżej):

public List < UserNameEmail > getAllUserNamesAndEmails() {
        return userRepository.findAll()
            .stream()
            .map(user - > new UserNameEmail(user.getName(), user.getEmail())
            .collect(Collectors.toList());
}


Warto zaznaczyć, że powyższy kod jest prawidłowy tylko w Javie 8! W wersjach wcześniejszych jest to jeszcze większy ból. Przykłady można by mnożyć, ale!

Są jeszcze inne plusy!

NPE mówimy bye, bye!

Ta…

Każdy chyba spotkał się z NullPointerException. Tzw. „one billion dollar mistake” bardzo łatwo zauważyć, ale niestety, nie zawsze tak samo łatwo naprawić. Kotlin stara się jak najbardziej zredukować tę ułomność Javy. Możliwe jest wręcz pozbycie się jej całkowicie. Przykładowo, mamy takiego JSON-a:

{
  "id": 1324534,
  "name": "GitHubLogin"
}

Po zmappowaniu JSON-a do Javy będzie to obiekt klasy Map<String, Object>. Chcemy odczytać wartość pola “name”. Po drodze będzie potrzebne zrzutowanie tego na typ String (oczywiście tylko wtedy, gdy to jest możliwe). W przypadku, gdy nie uda się nam tego zrobić, chcemy zwrócić pusty łańcuch tekstowy. Odczytując JSON-a nie mamy żadnej pewności, że ma wszystkie potrzebne nam wartości, więc ryzyko wystąpienia NPE nie jest takie małe. Jak to będzie wyglądało w obu językach?

Kotlin:

val githubLogin = (userMap[GlobalValues.GH_LOGIN] ?: "") as String


Java:

final String githubLogin;

if (userMap.get(GlobalValues.GH_LOGIN) != null) {
    githubLogin = (String) userMap.get(GlobalValues.GH_LOGIN);
} else {
    githubLogin = "";
}

Kolejny raz pojawia się tu problem z boilerplatem. Przy zachowaniu tych samych nazw zmiennych, w Javie musieliśmy napisać ponad 2 razy więcej znaków. Ponadto - rzutowanie w brzydkiej notacji. No i ten if, typowy dla programowania imperatywnego.

W Kotlinie kod jest wiele prostszy, a NPE nam nie grozi. Dlaczego? Użyliśmy operatora, który zwany jest potocznie “Elvis”.  W przypadku, gdy lewa część wyrażenia jest nie nullowa, zostanie ona zwrócona. W innym przypadku - ta prawa. O wiele czytelniej!

Samo rzutowanie odbywa się tutaj poprzez zapis “as nazwa_klasy”. Znacznie przejrzyściej. Pobieranie wartości z userMap także odbywa się w przyjemniejszy sposób - ale to już temat na osobny tekst.

Extension functions

Na pewno nie raz spotkałeś się z sytuacją, gdy miałeś zewnętrzną klasę, w której brakowało Ci metod. Fajnie byłoby dodać metodę do takiej klasy, prawda? Cóż, kod zazwyczaj nie jest dostępny wprost. Trzeba tworzyć swoją bibliotekę na bazie istniejącej, co jest totalnie niewarte zachodu. Może klasa FooUtils, która będzie miała statyczne metody zawsze przyjmie obiekt klasy Foo i będzie tworzyć magię? Jest to opcja. Kotlin z niej korzysta, ale w ładniejszym opakowaniu.

Extension functions to nic innego jak funkcje rozszerzające. Koncept jest znany z C#. Bardzo łatwo możemy dodać funkcję do istniejącej klasy. Wystarczy definicję funkcji poprzedzić nazwą klasy.  W samej funkcji możemy referować się do obiektu danej klasy poprzez słowo kluczowe this. Kolejny przykład usprawniający konfigurację kontrolerów w Springu:

Kotlin:

// dowolna_nazwa_pliku.kt
fun ViewControllerRegistry.addView(controllerData: Pair<String, String>) {
    this.addViewController(controllerData.first).setViewName(controllerData.second)
}

// uzycie

registry.addView(controllerData);

Java:

// zawzyczaj byłoby to ViewControllerRegistryUtils.java
public static void addView(ViewControllerRegistry registry, Pair < String, String > controllerData) {
    registry.addViewController(controllerData.getFirst()).setViewName(controllerData.getSecond());
}

// użycie

ViewControllerRegistryUtils.addView(registry, controllerData);

Owszem, w samej ilości kodu nie ma aż tak dużej różnicy. Owszem, pod spodem, ze względu na javowe ograniczenia, Kotlin sprowadza to do podobnej postaci. Natomiast sposób stosowania tego rozwiązania w Kotlinie oraz czytelność są zdecydowanie na plus. Z tego co wiem, funkcjonalność ta jest często stosowana przez programistów Androida. Tam co chwila klasy są rozszerzane, gdyż brakuje im wielu podstawowych metod, albo są wywoływane bardzo niewygodnie. Kotlin pozwala to bardzo uprościć.

Coroutines - programowanie współbieżne na nowo

Kotlin posiada dodatkową bibliotekę odpowiedzialną za coroutines. Dla osób, które nie wiedzą - coroutine (współprogram) to sekwencja instrukcji wykonywana w programie. Sekwencje mogą być przenoszone między innymi współprogramami. Główna różnica między wątkami, a współprogramami jest taka, że wątki to abstrakcja systemowa, natomiast współprogramy to abstrakcja na poziomie programu. Dzięki temu rozwiązaniu możemy łatwo komunikować się między konkretnymi współprogramami, jakość kodu nie cierpi (z API, jakie dostajemy tutaj jest wręcz przeciwnie!), czas wykonania również.

Nie chcę temu poświęcać dużo czasu, bo by nam go zabrakło (zobacz zresztą to „wprowadzenie” do tematu). Sam wszystkich możliwości jeszcze nie zbadałem, o czym dobrze świadczy sposób, w jaki użyliśmy coroutine’y w naszym programie - do asynchronicznego wysyłania maili. Działa to o wiele szybciej, niż gdybym robił to synchronicznie. Poza tym, sam zapis w najprostszej wersji jest bardzo prosty:

Kotlin:

users.forEach { user ->
  launch(CommonPool) {
    mailSender.sendEmail(user)
  }
}

Java (tylko 8 i wyżej):

for (User user: users) {
    new Thread(() - > {
        mailSender.sendEmail(user);
    }).start();
}

Warto również wiedzieć, że kotlinowe coroutines korzystają z wielu różnych konceptów (async, await, suspend, blocking). Możliwości i sposoby używania są przeogromne.

Funkcje jako obiekty

Podejście traktowania funkcji jako obiektów jest znane chociażby z JavaScriptu. Ma to wiele zastosowań w niektórych wzorcach projektowych, między innymi we wzorcu ,,strategia”. Ale po kolei. Jak w Kotlinie możemy przypisać funkcję do obiektu? Bardzo łatwo. Tak może wyglądać prosta operacja dodawania i jej użycie:

val add = { x: Int, y: Int -> x + y }

val twoPlusTwo = add(2, 2) // 4


Tak, tyle wystarczy. Kotlin jest w stanie domyślić się, jaki typ zwraca ta funkcja. Typ argumentów trzeba zdefiniować. W tym przypadku oba argumenty są typu Int. Kotlin wie, że to funkcja dzięki zapisowi w nawiasach klamrowych  { }. Jeżeli jest to jednolinijkowa funkcja, ostatnia wartość jest zwracana. W tym przypadku jest to wynik dodawania - bardzo prosta lambda.

Dobrze, ale jakiego typu jest zmienna add? Najpierw zobaczmy jak definiujemy typy dla funkcji w Kotlinie. Notacja w tym przypadku jest prosta:

(typ_argumentu_1, typ_argumentu_2,...) -> typ_argumentu_zwracanego


W tym przypadku, gdybyśmy chcieli zdefiniować typ wartości add, musielibyśmy to zapisać w ten sposób:

val add: (Int, Int) -> Int = { x: Int, y: Int -> x + y }


Jakiś bardziej przydatny sposób użycia? W naszym przypadku zastosowaliśmy tę funkcjonalność Kotlina do zaimplementowania wzorca, o którym mówiłem wcześniej. Na podstawie otrzymanej funkcji obliczana była punktacja uczestnika turnieju. Poniżej mamy trzy różne strategie jako zmienne oraz sposób użycia w innej funkcji.

Kotlin:

fun calculateDemo(args: Array<String>) {
    val absolutePoints = { goodAnswers: Int, badAnswers: Int -> goodAnswers - badAnswers }
    val oneAndHalfGoodWeight = { goodAnswers: Int, badAnswers: Int -> 1.5 * goodAnswers - badAnswers }
    val sixExtraPointsAtStart = { goodAnswers: Int, badAnswers: Int -> 6 + goodAnswers - badAnswers }

    val goodAnswers = 10
    val badAnswers = 2
    
    calculatePoints(goodAnswers, badAnswers, absolutePoints)
    calculatePoints(goodAnswers, badAnswers, oneAndHalfGoodWeight)    
    calculatePoints(goodAnswers, badAnswers, sixExtraPointsAtStart)
}

fun calculatePoints(goodAnswers: Int, badAnswers: Int, calculate: (Int, Int) -> Number) {
    println("Good answers $goodAnswers, badAnswers $badAnswers")
    println("Points: ${calculate(goodAnswers, badAnswers)}")
}

// result:

Good answers 10, badAnswers 2
Points: 8
Good answers 10, badAnswers 2
Points: 13.0
Good answers 10, badAnswers 2
Points: 14

Testy pisane jeszcze szybciej

Nie było to głównym zamysłem autorów języka, ale w Kotlinie da się pisać testy jeszcze szybciej. Oczywiście, są już odpowiednie biblioteki, które to usprawniają, ale zostańmy przy samym Kotlinie.

Pierwszą irytującą rzeczą przy testowaniu aplikacji napisane w Javie są nazwy funkcji w testach. Nie raz spotkaliście się na pewno z funkcjami typu:

shouldReturnSomethingWhenFirstValueIsTenAndSecondValueIsNotNull. 

Mega długie, mega problematyczne w czytaniu, mega brzydkie.

W Kotlinie możemy escape’ować niektóre słówka kluczowe poprzez użycie znaków ``. Niektóre nazwy funkcji w Javie albo innych znanych bibliotekach są słówkami kluczowym w Kotlinie, np. System.in albo Mockito.when. Jesteśmy więc zmuszeni do zapisu System.`in` albo Mockito.`when` (in oraz when są słowami kluczowymi w Kotlinie). Mógłbyś pomyśleć - jak to się ma do usprawniania testów?

Ano, możemy tworzyć własne funkcje escapując niektóre niedozwolone znaki, np. spacje. Mądre IDE nie będzie miało problemów z wyświetlaniem takiej funkcji jako normalny tekst w różnych raportach - co jest bardzo istotne jeżeli chodzi o testy. Prosty przykład z naszej aplikacji:

Kotlin:

@Test

fun `should unblock user in tournament in which user participates when request author is an owner`() {

    // code

}

Java:

@Test
public void shouldUnblockUserInTournamentInWhichUserParticipatesWhen RequestAuthorIsAnOwner() {

    // code

}

Drobnostka, a cieszy oko.

Kotlin posiada również dużo ciekawych funkcji w standardowej bibliotece, takie jak let, also, run, apply. Nie będę się o nich rozpisywać - wszystkie są podobne, ale mają różne zastosowania. Jeśli Cię to interesuje, odsyłam Cię do tego świetnego artykułu.

Skupię się na funkcji apply. Intuicyjnie pewnie domyślasz się do czego może służyć. Funkcja ta może być wywołana na jakimkolwiek obiekcie. Przyjmuje ciąg funkcji, które mają być wywoływane -  z tym, że kontekstem this jest obiekt, na którym wołana jest funkcja apply. Funkcja apply zwraca (zmodyfikowany) obiekt, na którym jest wołana. Głównie używa się jej przy tworzeniu obiektu i wywoływania ciągu funkcji (zazwyczaj setterów). Bardzo przydatne również, gdy odwołujemy się cały czas do obiektu na przestrzeni paru linijek, czyli np. do asercji paru pól tego samego obiektu. Kolejny przykład z aplikacji:

Kotlin:

// then
foundTournamentsPage.apply {
    Assert.assertEquals(1, totalElements)
    Assert.assertEquals(1, totalPages)
    Assert.assertEquals(TournamentStatus.ACTIVE, content[0].status)
    Assert.assertEquals(tournamentToFindName, content[0].name)
}

Java:

// then

Assert.assertEquals(1, foundTournamentsPage.totalElements)
Assert.assertEquals(1, foundTournamentsPage.totalPages)
Assert.assertEquals(TournamentStatus.ACTIVE, foundTournamentsPage.content[0].status)
Assert.assertEquals(tournamentToFindName, foundTournamentsPage.content[0].name)

Dotychczasowe problemy z Kotlinem

Klasy finalne

Klasy w Kotlinie domyślnie mają modyfikator final, nie mogą być rozszerzane. Aby umożliwić dziedziczenie, należy dodać przed definicją klasy słówko kluczowe open. Problem pojawia się np., gdy używamy biblioteki Spring. Klasy oznaczone adnotacją @Configuration lub @Service nie mogą być finalne, a takich klas zazwyczaj jest dużo. Trochę bez sensu przy każdej klasie dodawać dodatkowy modyfikator.

Jest na to jednak proste rozwiązanie. Możemy, chociażby za pomocą gradle’a, dodać “all-open compiler plugin”. Jest to plugin, który na poziomie kompilacji dodaje do każdej klasy modyfikator open. Szkoda jednak pozbywać się tej funkcjonalności w zupełności, jeżeli nie musimy. Dla Springa istnieje już odpowiedni plugin (de facto, dodawany do podstawowego projektu wygenerowanego za pomocą Spring Initializera, który używa Gradle’a i Kotlina). Słówko open jest dodawane tylko do klas oznaczonych odpowiednimi adnotacjami. Sami również możemy zdefiniować jakie klasy mają być ,,otwierane” przez plugin. Po więcej informacji zapraszam na stronę Kotlina.

Wszędzie pytajniki

Nadal będę bazować na często używanej przeze mnie bibliotece Spring. Tak jak mówiłem, jeżeli pole może mieć wartość null, musimy to zaznaczyć przy definicji zmiennej. Załóżmy, że wstrzykujemy zależności przez pole (tak, wiem, nie powinno się, ale czysto hipotetycznie). Niech będzie to springowe ziarno typu MyBean:

@Service
class MyService {

    @Autowired
    var myBean: MyBean
}

To nie zadziała. Dlaczego? Jeżeli pole oznaczamy jako non-nullable, musimy wtedy przypisać do niego wartość w konstruktorze. W tym przypadku nie odbywa się to, a więc w pewnym momencie jednak będzie wartość null. Musimy więc zmienić zapis definicji pola na:

@Autowired
var myBean: MyBean?

Nie jest to do końca ok. Dlaczego? Gdyż musimy teraz za każdym razem sprawdzać, czy myBean nie ma wartości null. Wywołanie funkcji za każdym razem będzie wyglądać tak:

myBean?.fun1()
myBean?.fun2()
myBean?.fun3()

Brzydko. Na szczęście, mamy w Kotlinie słówko kluczowe lateinit. Oznacza to, że zmienna będzie później zainicjalizowana i nie potrzebujemy sprawdzać za każdym razem, czy nie ma nulla. Kod zmieni się wtedy w ten sposób:

@Autowired
lateinit var myBean: MyBean

myBean.fun1()
myBean.fun2()
myBean.fun3()

Sytuacja ta ma też często miejsce przy pisaniu testów, gdzie inicjalizacja zmiennych odbywa się w odpowiednich funkcjach - a więc uczulam na to i podaję proste rozwiązanie.


Podsumowanie

Kotlin w ostatnim czasie jest ,,wow” nie bez powodu - daje nam ogromne możliwości. Kod jest bardziej czytelny, jednocześnie mniej czasu tracimy na jego napisanie. Posiada ogrom funkcjonalności, których w Javie bardzo brakuje.

Czy żałuję, że przeszedłem na Kotlina? Żałuję, że tak późno. Oczywiście, z Javą styczność mam i mieć będę, natomiast z mojej perspektywy mogę powiedzieć - oby jak najmniej! Po paru miesiącach obcowania z Kotlinem, gdy trzeba napisać coś w Javie nie chce się czasem uwierzyć, że w tym szeroko używanym języku  nie da się niektórych rzeczy zrobić tak łatwo i intuicyjnie. Źródeł do nauki nie brakuje, rozgłosu również. Wróżę Kotlinowi i całemu ekosystemowi związanemu z tym językiem świetlaną przyszłość.

Jeżeli chcesz dowiedzieć się o Kotlinie więcej i zacząć go używać w swoich projektach, gorąco zapraszam Cię na tę stronę po jeszcze większy zastrzyk wiedzy. :)  

<p>Loading...</p>