Kotlin DSL, 指用Kotlin寫的Domain Specific Language. 本文經過解析官方的Kotlin DSL寫html的例子, 來講明Kotlin DSL是什麼.html
首先是一些基礎知識, 包括什麼是DSL, 實現DSL利用了那些Kotlin的語法, 經常使用的情形和流行的庫.java
對html實例的解析, 沒有一衝上來就展現正確答案, 而是按照分析需求, 設計, 和實現細化的步驟來逐步讓解決方案變得明朗清晰.android
DSL: Domain Specific Language. 專一於一個方面而特殊設計的語言.git
能夠看作是封裝了一套東西, 用於特定的功能, 優點是複用性和可讀性的加強. -> 意思是提取了一套庫嗎?github
不是.數據庫
DSL和簡單的方法提取不一樣, 有可能代碼的形式或者語法變了, 更接近天然語言, 更容易讓人看懂.安全
作一個DSL, 改變語法, 在Kotlin中主要依靠:bash
三個lambda語法:服務器
it
直接表示.()
外面. 若是lambda是惟一的參數, 能夠省略小括號()
.擴展方法.網絡
Gradle的build文件就是用DSL寫的. 以前是Groovy DSL, 如今也有Kotlin DSL了.
還有Anko. 這個庫包含了不少功能, UI組件, 網絡, 後臺任務, 數據庫等.
和服務器端用的: Ktor
應用場景: Type-Safe Builders type-safe builders指類型安全, 靜態類型的builders.
這種builders就比較適合建立Kotlin DSL, 用於構建複雜的層級結構數據, 用半陳述式的方式.
官方文檔舉的是html的例子. 後面就對這個例子進行一個梳理和解析.
首先明確一下咱們的目標.
作一個最簡單的假設, 咱們期待的結果是在Kotlin代碼中相似這樣寫:
html {
head { }
body { }
}
複製代碼
就能輸出這樣的文本:
<html>
<head>
</head>
<body>
</body>
</html>
複製代碼
仔細觀察第一段Kotlin代碼, html{}
應該是一個方法調用, 只不過這個方法只有一個lambda表達式做爲參數, 因此省略了()
.
裏面的head{}
和body{}
也是同理, 都是兩個以lambda做爲惟一參數的方法.
由於標籤的層級關係, 能夠理解爲每一個標籤都負責本身包含的內容, 父標籤只負責按順序顯示子標籤的內容.
因爲<head>
和<body>
等標籤只在<html>
標籤中才有意義, 因此應該限制外部只能調用html{}
方法, head{}
和body{}
方法只有在html{}
的方法體中才能調用.
由於標籤看起來都是相似的, 爲了代碼複用, 首先設計一個抽象的標籤類Tag
, 包含:
文字比較特殊, 它不帶標籤符號<>
, 就輸出本身. 因此它的渲染方法就是輸出文字自己.
能夠提取出一個更加基類的接口Element
, 只包含渲染方法. 這個接口的子類是Tag
和TextElement
.
有文字的標籤, 如<title>
, 它的輸出結果:
<title>
HTML encoding with Kotlin
</title>
複製代碼
文字元素是做爲標籤的一個子標籤的. 這裏的實現不容易本身想到, 直接看後面的實現部分揭曉答案吧.
有了前面的心路歷程, 再來看實現就能容易一些.
首先是最基本的接口, 只包含了渲染方法:
interface Element {
fun render(builder: StringBuilder, indent: String)
}
複製代碼
它的直接子類標籤類:
abstract class Tag(val name: String) : Element {
val children = arrayListOf<Element>()
val attributes = hashMapOf<String, String>()
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, indent + " ")
}
builder.append("$indent</$name>\n")
}
private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in attributes) {
builder.append(" $attr=\"$value\"")
}
return builder.toString()
}
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
複製代碼
完成了自身標籤名和屬性的渲染, 接着遍歷子標籤渲染其內容. 注意這裏爲全部子標籤加上了一層縮進.
initTag()
這個方法是protected
的, 供子類調用, 爲本身加上子標籤.
帶文字的標籤有個抽象的基類:
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
複製代碼
這是一個對+
運算符的重載, 這個擴展方法把字符串包裝成TextElement
類對象, 而後加到當前標籤的子標籤中去.
TextElement
作的事情就是渲染本身:
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
複製代碼
因此, 當咱們調用:
html {
head {
title { +"HTML encoding with Kotlin" }
}
}
複製代碼
獲得結果:
<html>
<head>
<title>
HTML encoding with Kotlin
</title>
</html>
複製代碼
其中用到的Title
類定義:
class Title : TagWithText("title")
複製代碼
經過'+'運算符的操做, 字符串: "HTML encoding with Kotlin"被包裝成了TextElement
, 他是title標籤的child.
對外的公開方法只有這一個:
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
複製代碼
init
參數是一個函數, 它的類型是HTML.() -> Unit
. 這是一個帶接收器的函數類型, 也就是說, 須要一個HTML
類型的實例來調用這個函數.
這個方法實例化了一個HTML
類對象, 在實例上調用傳入的lambda參數, 而後返回該對象.
調用此lambda的實例會被做爲this
傳入函數體內(this
能夠省略), 咱們在函數體內就能夠調用HTML
類的成員方法了.
這樣保證了外部的訪問入口, 只有:
html {
}
複製代碼
經過成員函數建立內部標籤.
HTML類以下:
class HTML : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
複製代碼
能夠看出html
內部能夠經過調用head
和body
方法建立子標籤, 也能夠用+
來添加字符串.
這兩個方法原本能夠是這樣:
fun head(init: Head.() -> Unit) : Head {
val head = Head()
head.init()
children.add(head)
return head
}
fun body(init: Body.() -> Unit) : Body {
val body = Body()
body.init()
children.add(body)
return body
}
複製代碼
因爲形式相似, 因此作了泛型抽象, 被提取到了基類Tag
中, 做爲更加通用的方法:
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
複製代碼
作的事情: 建立對象, 在其之上調用init lambda, 添加到子標籤列表, 而後返回.
其餘標籤類的實現與之相似, 不做過多解釋.
以上都寫完了以後, 感受大功告成, 但其實還有一個隱患.
咱們竟然能夠這樣寫:
html {
head {
title { +"HTML encoding with Kotlin" }
head { +"haha" }
}
}
複製代碼
在head方法的lambda塊中, html塊的receiver仍然是可見的, 因此還能夠調用head
方法. 顯式地調用是這樣的:
this@html.head { +"haha" }
複製代碼
可是這裏this@html.
是能夠省略的.
這段代碼輸出的是:
<html>
<head>
haha
</head>
<head>
<title>
HTML encoding with Kotlin
</title>
</head>
</html>
複製代碼
最內層的haha反卻是最早被加到html對象的孩子列表裏.
這種穿透性太混亂了, 容易致使錯誤, 咱們能不能限制每一個大括號裏只有當前的對象成員是可訪問的呢? -> 能夠.
爲了解決這種問題, Kotlin 1.1推出了管理receiver scope的機制, 解決方法是使用@DslMarker
.
html的例子, 定義註解類:
@DslMarker
annotation class HtmlTagMarker
複製代碼
這種被@DslMarker
修飾的註解類叫作DSL marker
.
而後咱們只須要在基類上標註:
@HtmlTagMarker
abstract class Tag(val name: String)
複製代碼
全部的子類都會被認爲也標記了這個marker.
加上註解以後隱式訪問會編譯報錯:
html {
head {
head { } // error: a member of outer receiver
}
// ...
}
複製代碼
可是顯式仍是能夠的:
html {
head {
this@html.head { } // possible
}
// ...
}
複製代碼
只有最近的receiver對象能夠隱式訪問.
本文經過實例, 來逐步解析如何用Kotlin代碼, 用半陳述式的方式寫html結構, 從而看起來更加直觀. 這種就叫作DSL.
Kotlin DSL經過精心的定義, 主要的目的是爲了讓使用者更加方便, 代碼更加清晰直觀.
More resources: