Od kilku(nastu) lat jesteśmy świadkami małej rewolucji w dziedzinie aplikacji sieciowych. Koncepcja zwracania gotowych dokumentów HTML jest wypierana przez dynamiczne aplikacje JS generujące widoki po stronie klienta. Nie znaczy to jednak że tradycyjne podejście odeszło całkowicie do lamusa. Tak naprawdę jest to wciąż ogromna część serwisów sieciowych. Dziś weźmiemy na warsztat porównanie dwóch takich rozwiązań.

Jeszcze kilka lat temu gdy generowanie widoków po stronie serwera było najpowszechniejszą praktyką, za najlepsze i najnowocześniejsze rozwiązanie uchodziły mechanizmy szablonów. W najbliższym mi świecie JVM mamy dwa klasyczne i oficjalne ich standardy - JSP i JSF. JSP było powszechnie nielubiane i krytykowane głównie przez bałagan z pomieszanym kodem HTML i Javy. Ich następca zaś - JSF - był ściśle zintegrowany z resztą JEE które to zostało wyparte na rynku przez Springa przez co samo JSF nie zdobyło większej popularności. Wydaje mi się że przyłożyła się do tego także specyfika architektoniczna. W SpringMVC każdemu żądaniu HTTP odpowiada jedna metoda kontrolera (request-based MVC) przez co łatwo jest programiście zarządzać uwierzytelnieniem i logiką żądania oraz debugować - wszystko jest w jednym miejscu. JSF zaś to rozwiązanie bazujące na rozproszonych komponentach ManagedBeans (component-based MVC) - ciekawe ale i znacznie mniej wygodne. Mało kto wie że Oracle w JEE 8 (2017 r.) zaprezentowało także podejście bazujące na żądaniach (JSR 371 - JEE MVC) lecz nie zdobyło ono większej popularności. Rozwiązania te, choć wciąż można je gdzieniegdzie spotkać, zostały dość skutecznie wyparte przez nieoficjalne technologie takie jak FreeMarker czy Thymeleaf - szczególnie w połączeniu ze Spring MVC. I choć generowanie widoków po stronie serwera traci na popularności to wciąż są to powszechne standardy.

Od kilkunastu więc lat standardem tworzenia widoków po stronie serwera są szablony. Nikt nie będzie przecież sklejać stringów w kontrolerze, prawda? Rozwiązania generujące kod HTML przy pomocy obiektów reprezentujących elementy HTML miały zawsze marginalną popularność z uwagi na niską wygodę ich użycia oraz niepodobieństwo do kodu HTML. A raczej było tak aż do czasu wprowadzenia w Kotlinie mechanizmu DSL.  

Jeżeli ktoś nie wie czym jest Kotlinowe DSL to już śpieszę z krótkim wyjaśnieniem. Ogólnie DSL to tak zwany język dziedzinowy czyli język specyficzny dla danego, wąskiego zastosowania - w przeciwieństwie do języków ogólnego przeznaczenia (GPL). Przykładem może być AWK czy chociażby język wyrażeń regularnych. Sam HTML jest także DSLem tak samo jak języki szablonów. Czym zatem odróżnia się Kotlinowy DSL? Otóż jest to tzw wewnętrzny DSL czyli język dziedzinowy wewnątrz języka ogólnego przeznaczenia jakim jest Kotlin. Nie jest to zatem odrębny język a raczej jego podzbiór pod konkretne zastosowanie. A przynajmniej tak jest określany przez jego twórców bo moim zdaniem są to po prostu ciekawe elementy składni języka umożliwiający wygodne tworzenie obiektów: funkcje rozszerzające, lambdy z odbiorcami oraz wysuwanie lambd poza nawiasy. Można przez to tworzyć wiele różnych "DSLi" pod konkretne zastosowania. Zresztą spójrzcie na ten banalny i uproszczony przykład:

data class Document(var attribute: String? = null, var subElement: SubElement? = null)
data class SubElement(var subattribute: String? = null)

fun document(block: Document.() -> Unit): Document {
val document = Document()
block(document)
return document
}

fun Document.subElement(block: SubElement.() -> Unit): Unit {
subElement = SubElement().apply(block)
}

// example of use
document {
attribute = "A"

subElement {
subattribute = "B"
}
}

Kluczowymi elementami tego rozwiązania są tu oczywiście parametry "block" o typach Document.() -> Unit oraz SubElement.() -> Unit. Są to po prostu lamby z odbiorcami które można wywołać na obiektach klasy Document i SubElement (przyjmujące de facto taki obiekt jako parametr). Dzięki tej prostej sztuczce komponowanie obiektów (np reprezentantów strony HTML) wygląda prawie tak dobrze jak szablon (przypomina to bardzo składnię JSONa) a jednocześnie cały czas zostajemy w języku programowania ze wszystkimi jego dobrodziejstwami. No ale zobaczmy jakiś przykład z prawdziwego zdarzenia. Strona główna Hex.log tworzona jest obecnie przy pomocy takiego szablonu:

<!DOCTYPE html>
<html th:lang="${#locale}">
<head th:replace="head.html :: head"></head>

<body onresize="resize_posts()">
<header th:replace="header.html :: header"></header>
<div class="main-body">
<br>
<!--/*@thymesVar id="posts" type="java.util.List<domain.Post>"*/-->
<div class="post" th:each="post, stat: ${posts}">
<!--/*@thymesVar id="post" type="domain.Post"*/-->
<button th:id="${post.id.value}" class="post-header openable" onclick="toggle_collapse(this)" th:onauxclick="|window.open('${#locale}/post/${post.id.value}')|">
<p>
<span th:text="${#temporals.format(post.createDate, 'dd.MM.yyyy')}" class="post-date"></span>
<span th:text="${post.title}" class="post-title"></span>
</p>
<p th:text="${post.shortcut}"></p>
</button>
<div class="post-panel"></div>
</div>
<br>
</div>
</body>
</html>
Odpowiednikiem tego w kotlinowym DSLu jest zaś taki kod:

fun mainPage(language: Language, posts: Array<Post>): HTML.() -> Unit = page(language) {
br()
posts.forEach { post ->
div("post") {
button(classes = "post-header openable") {
id = "${post.id?.value}"
onClick = "toggle_collapse(this)"
attributes["onAuxClick"] = "window.open('/${language}/post/${post.id?.value}')"
p {
span("post-date") { +post.createDate.formatted() }
span("post-title") { +post.title }
}
p { +post.shortcut }
}
div("post-panel")
}
}
}

I jak się Wam to podoba? Moim zdaniem jest to całkiem znośne choć ma też swoje wady. Porównajmy więc oba rozwiązania:

Do wymienionych wyżej argumentów można dorzucić także to, że w podejściu szablonów zawsze trzeba mieć komplet danych, których zebranie może być ciężkie a niekoniecznie muszą być one wymagane jeżeli ich użycie jest objęte instrukcją warunkową. Można co prawda użyć leniwych wrapperów ale jest to kolejna uciążliwość. W takich przypadkach często pisze się różne szablony. W KDSL takiego problemu nie ma. Wszystko może być wywołane warunkowo podczas komponowania widoku.

Jak wygląda więc porównanie obu rozwiązań? Osobiście oceniam KDSL jako lepsze rozwiązanie. Jest co prawda zazwyczaj trochę brzydsze lecz daje większą swobodę komponowania i lepsze sterowanie przepływem. Cieszę się, że twórcy ktor-html-buildera dali możliwość wstawiania niestandardowych atrybutów. Bez tego nie dałoby się wygodnie zaimplementować na Hex.log otwierania wpisu w nowym oknie przez kliknięcie środkowym przyciskiem na belkę tytułową (zauważyliście tę funkcjonalność?).

Czy zatem można już obwieszczać zmierzch szablonow? Wbrew prawu nagłówków Betteridge’a uważam że w znacznej mierze tak. Nie tylko zresztą dla generatorów HTML ale w gruncie rzeczy wszystkich tego typu dokumentów - XML, JSON, YAML itp. Wiadomo, że nie stanie się to od razu - szablony nie są tragicznym rozwiązaniem, tylko istnieją lepsze alternatywy. Myślę, że z czasem wyjdą one z użycia a programowanie w XMLach i szablonach będzie postrzegane na równi z instrukcją GOTO. 

Zmianę tę wprowadzam także na Hex.log. Wystawiłem na Githubie rewizję wymiany Thymeleaf na ktor-html-buildera. Zachęcam do jej przejrzenia i skomentowania. Dziękuję tutaj także moim znajomym z grupy Hex.log Reviewers: Gosi i Grześkowi za jej przejrzenie. Jeżeli chcesz do nich dołączyć i pomagać w rozwoju tego bloga to napisz do mnie!