Backend

Kotlin. Domain Specific Language w praktyce

kotlin

Na początku warto by było sobie wyjaśnić, co oznacza ten enigmatyczny skrót – „DSL”. DSL (Domain Specific Language) czyli język dziedzinowy, dedykowany. Jak wskazuje rozwinięcie skrótu, DSL ma się sprawdzić bardzo dobrze w jednej konkretnej domenie i dziedzinie problemów. Z reguły DSL nie jest tak rozbudowany jak języki programowania ogólnego zastosowania.

Zapytacie – po co komu język, który ma być użyteczny tylko do jednej domeny? Przecież mamy mnóstwo języków, które są ogólnego przeznaczenia i pozwalają na napisanie wszystkiego, czego sobie zapragniemy.

Michał Sieniawski. BE Developer ze Stepwise. Programista Kotlin w Stepwise. Choć nie boi sobie również ubrudzić rąk zajmując się DevOpsem bądź Cloudem. Jeśli znajdzie dużą sumę pieniędzy kupi dom w górach i zajmie się hodowlą owiec. Oczywiście jeśli będzie tam szybki internet. W końcu ktoś musi udostępniać obrazy Debiana i robić relaye Tora.


Oczywiście, że możemy w nich napisać wszystko. Natomiast to, że można nie oznacza, że w każdym z zastosowań będą się sprawdzały świetnie, szczególnie jeśli chodzi o łatwość wyrażania wymagań. Weźmy tu za przykład komunikację z bazami danych. Bez wątpienia do komunikacji z bazami danych można by było, i jest to wykorzystywane, użyć języków ogólnego przeznaczenia w postaci, chociażby Javy, czy Pythona. Natomiast temu, komu przyszło kiedykolwiek tego dokonać, wie jak szybko pragnął wrócić do SQLa. To jest właśnie przykład języka zaprojektowanego tylko i wyłącznie do jednej domeny i sądząc, po jego historii idzie mu to świetnie. Co więcej, nawet mógłbym się pokusić o stwierdzenie, że będzie mu to szło świetnie jeszcze przez długi, długi czas.

Języki wewnętrzne i zewnętrzne

W tym miejscu należy jeszcze rozróżnić jedną kluczową kwestię jeśli chodzi o języki konkretnej domeny, otóż można wyróżnić ich dwa rodzaje:

  • zewnętrzne – języki, które mają swoją własną składnię – dwa najpopularniejsze to, przytoczony chwilę temu, SQL oraz CSS,
  • wewnętrzne – języki, które nie mają swojej odrębnej składni. Ich składania oparta jest o składnię języka, za pomocą którego się go definiuje, a celem istnienia jest poprawa czytelności. W świecie JVM-a najczęściej mamy z nimi do czynienia pod postacią tzw. fluent interface, prezentujących się następująco:

Asercja z zastosowaniem biblioteki AssertJ:

Oczywiście nic nie stoi na przeszkodzie, żeby z takich konstrukcji korzystać w dowolnym języku. Natomiast dzisiaj zajmiemy się typowo programowaniem w Kotlinie i wewnętrznym Domain Specific Language.

Kotlin i DSL – tutorial

I to jest dobry moment, aby płynnie przejść do tutorialu i kodu. W końcu jak lepiej nauczyć się jakiegoś pojęcia jak nie zacząć go praktykować. Załóżmy, że mamy takie wymagania biznesowe: mamy wiele różnych kolejek i musimy zdefiniować konfigurację komunikacji między nimi na takiej zasadzie, że jedna kolejka przyjmuje wiadomość, następnie sprawdza wśród swojej listy odbiorców czy któryś z nich akceptują taką wiadomość i jeśli tak, to wysyła.

Konfiguracja

Konfiguracja może tutaj mieć następujące informacje: id flow, nazwę kolejki źródłowej, miejsca docelowe. Każde z miejsc docelowych ma swoją własną listę procesorów, które jak nazwa sugeruje, manipulują danymi wejściowymi według wskazanej kolejności. Tutaj zastrzeżenie – jest to oczywiście tylko przykład i w rzeczywistym programie wyglądałoby to najprawdopodobniej zdecydowanie inaczej.

Gdybyśmy mieli tę konfigurację przestawić za pomocą fluent interface w Kotlinie. Mogłaby się prezentować w następujący sposób:

Konfiguracja za pomocą DSLa

Teraz kiedy już wiemy, jak wygląda postać ze „standardowego” podejścia możemy utworzyć analogiczną konfigurację za pomocą DSLa w Kotlinie:

Nie zamierzam tutaj nikogo przekonywać odnośnie tego, która z wersji jest czytelniejsza. Oczywiste jest, że jest to subiektywna sprawa. Natomiast jedną rzeczą, którą chciałbym, tu podkreślić jest to, że obie wersje są bardzo czytelne. Nawet bez spoglądania w jakiekolwiek wymagania możemy się naprawdę sporo domyśleć odnośnie logiki. ”Nadmiarowe” słowa w pierwszym przykładzie: broadcast, single, inOrder pomimo tego, że nie są, tam niezbędne w mojej opinii tylko poprawiają czytelność.

Kotlin, tworzenie DSLi – wyjaśnienie

Teraz kiedy widzieliśmy obie wersje, możemy przejść do wyjaśnienia, jakie mechanizmy języka umożliwiają Kotlinowi tworzenie DSL.

Wyjaśnienia odnośnie składni:

Dozwolone jest pominięcie nawiasów klamrowych w przypadku metod, które posiadają w swoim ciele jedną instrukcję. Wówczas w miejscu otwierającego ciało metody nawiasu klamrowego używa się znaku równa się i oczywiście pomija się zamykający nawias klamrowy. Dodatkowo kiedy zastosuje się taki styl, można również pominąć definicję zwracanego typu przez metodę. Co skutkowałoby takim zapisem:

Natomiast nie jest to coś, czego używamy na co dzień w projektach. Ustaliliśmy wewnętrzną zasadę, że w przypadku gdy chcemy skorzystać ze skróconego zapisu i pominąć nawiasy zawsze definiujemy, jaki jest typ zwracany przez metodę. Z naszego doświadczenia jawne definiowanie typu przy ciele metody poprawia czytelność kodu.

Kotlin pozwala definiować funkcje jako top level function więcej o nich można przeczytać tu – a, w skrócie mówiąc: są to funkcje niezawierające się w żadnej klasie. pipelineConfiguration jest tego przykładem.

W momencie, w którym ostatnim argumentem funkcji jest funkcja wyższego rzędu, tak jak to jest w pipelineConfiguration język zachęca nas do tego, abyśmy przy jej wywołaniu zdefiniowali ciało funkcji argumentu w następujący sposób:

Funkcja wyższego rzędu

A teraz przeanalizujmy kluczowy fragment z perspektywy tworzenia DSL:

Przede wszystkim jest to funkcja wyższego rzędu (ang. higher order function) – czyli taka funkcja, która jest przekazywana do innej funkcji jako argument. Po więcej informacji o nich odsyłam do pierwszego artykułu z serii o Kotlinie, który jest poświęcony właśnie nim.

Dla nas w tym momencie kluczowe będzie to jak się je definiuje np.:

I przykład użycia prezentowałby się następująco:

“Ale! ale!” pomyślicie sobie teraz. “Ta definicja różni się od tej powyżej – tutaj nie ma typu i kropki przed ()”. I to jest bardzo trafne spostrzeżenie. Otóż tam definiujemy to, że funkcja wywoływana będzie na konkretnym typie. Prześledźmy to na innym przykładzie:

Teraz definicja funkcji pipelineConfiguration powinna być jasna.

Dla pełni zrozumienia konceptu zaprezentuję jeszcze klasę pipelineConfiguration:

Definiujemy klasę, która jest typu data (więcej na ten temat tutaj) zawiera ona w sobie klasę Builder, której pola wypełnialiśmy w przykładzie z kolejkami. Mamy dodatkowo jeszcze metodę build, która przepisuje wartości z buildera do właściwej zwracanej klasy. Dzięki czemu końcowy obiekt jest immutable.

Destination

I oczywiście zostaje nam jeszcze jedna klasa do analizy – Destination.

Zasadniczo jest ona bardzo podobno do klasy PipelineConfiguration natomiast posiada ona kilka smaczków, przy których warto by było spędzić chwilę.

Zacznijmy od tego, że wykorzystano tutaj zmienną konfiguracyjną w metodzie build o nazwie ANONYMIZE. Możemy o niej myśleć jako o zmiennej globalnej, której wczytywana jest z jakiejś konfiguracji. W momencie, w którym ta flaga zmienia się na true dodajemy do listy procesorów w pierwszym etapie walidację.

Drugą rzeczą, na którą warto spojrzeć, w tej klasie jest metoda o nazwie processor. Sama w sobie nie jest zbytnio ciekawa, natomiast pokazuje, że można również używać nazw metod do modyfikacji nazw, które DSL zmienia. W tym przypadku zmienna w klasie Destination nazywa się processors, co nie byłoby najlepszą nazwą do odwoływania się w DSL. Wobec czego tworzymy metodę o nazwie processor, która jest dużo bardzo adekwatniejsza do zastosowania.

Zastosowanie lateinit var powoduje, że pole staje się wymaganym, gdyż w przypadku jego niewypełnienia i wywołania metody build() rzucony zostanie Null Pointer Exception.

Kotlin DSL tutorial – podsumowanie

I w zasadzie to już wszystko, co jest nam potrzebne do definiowania DSL w Kotlinie. Jak się przekonaliśmy, nie jest zbyt skomplikowana, żeby nie powiedzieć wręcz prosta. A w ramach zaznajomienia się z częścią językową zachęcam do napisania jakiegoś prostego przykładu DSL, dzięki któremu z pewnością znacznie szybciej i lepiej zrozumie się prezentowane powyżej idee.

Podsumujmy sobie, czego się dowiedzieliśmy.

Tworząc DSL w Kotlinie, niejako tworzymy własny język, który w jednym zastosowaniu – jednej domenie, ma się sprawdzić zdecydowanie lepiej niż Kotlin. Chodzi tutaj o rozbudowane, drzewiaste struktury, przy których stosowanie fluent interface mogłoby się okazać nieczytelne. Dodatkowo zyskujemy tutaj separację między definicją konfiguracji a tym, do czego jest ona później wykorzystywana. Co zdecydowanie wpływa na utrzymywalność kodu.


Zdjęcie główne artykułu pochodzi z unsplash.com.

Podobne artykuły