《Kotin 極簡教程》第14章 使用 Kotlin DSL
第14章 使用 Kotlin DSL
最新上架!!!《 Kotlin極簡教程》 陳光劍 (機械工業出版社)
可直接打開京東,淘寶,當當===> 搜索: Kotlin 極簡教程
https://www.jianshu.com/p/35b487734339
我們在前麵的章節中,已經看到了 Kotlin DSL 的強大功能。例如Gradle 的配置文件 build.gradle (Groovy),以及前麵我們涉及到的Gradle Script Kotlin(Kotlin)、Anko(Kotlin)等,都是 DSL。我們可以看出,使用DSL的編程風格,可以讓程序更加簡單幹淨、直觀簡潔。當然,我們也可以創建自己的 DSL。
本章就讓我們一起來學習一下 Kotlin中 DSL的相關內容。
我們在上一章中已經看到了在 Android 中使用下麵這樣的 嵌套DSL 風格的代碼來替代 XML 式風格的視圖文件
UI {
// AnkoContext
verticalLayout {
padding = dip(30)
var title = editText {
// editText 視圖
id = R.id.todo_title
hintResource = R.string.title_hint
}
var content = editText {
id = R.id.todo_content
height = 400
hintResource = R.string.content_hint
}
button {
// button 視圖
id = R.id.todo_add
textResource = R.string.add_todo
textColor = Color.WHITE
setBackgroundColor(Color.DKGRAY)
onClick { _ -> createTodoFrom(title, content) }
}
}
}
相比 XML 風格的 DSL(XML 本質上講也是一種 DSL),明顯使用原生的編程語言(例如Kotlin)DSL 風格更加簡單幹淨,也更加自由靈活。
Kotlin DSL 的編程風格是怎樣的呢?以及其背後實現的原理是怎樣的呢?下麵就讓我一起來探討一下。
DSL 是什麼
DSL(Domain-Specific Language,領域特定語言)指的是專注於特定問題領域的計算機語言(領域專用語言)。不同於通用的計算機語言(GPL),領域特定語言隻用在某些特定的領域。
“領域”可能特指某一產業,比如保險、教育、航空、醫療等;它也可能是指一種方法或者技術,比如JavaEE、.NET、數據庫、服務、消息、架構以及領域驅動設計。開發DSL語言的目的在於,我們要以一種更優雅、更簡潔的方式麵對領域中的一些列挑戰。之所以能夠這樣,是因為這個語言剛好夠用於解決領域中唯一存在的一係列挑戰,一點兒不多、也一點兒不少,剛剛好。
比如用來顯示網頁的HTML語言。更加典型的例子是Gradle,它基於Ant 和 Maven,使用基於Groovy的DSL 來聲明項目構建配置 build.gradle,而不是傳統的XML。
DSL 簡單講就是對一個特定問題 (受限的表達能力) 的方案模型的更高層次的抽象表達(領域語言),使其更加簡單易懂 (容易理解的語義以及清晰的語義模型)。
DSL 隻是問題解決方案模型的外部封裝,這個模型可能是一個 API 庫,也可能是一個完整的框架等等。DSL 提供了思考特定領域問題的模型語言,這使得我們可以更加簡單高效地來解決問題。DSL 聚焦一個特定的領域,簡單易懂,功能極簡但完備。DSL 讓我們理解和使用模型更加簡易。
DSL 有內部 DSL 跟外部 DSL 之分。例如 Gradle、Anko 等都是我們使用通用編程語言(Java 和 Kotlin)創建的內部DSL。
內部DSL
內部DSL是指與項目中使用的通用目的編程語言(Java、C#或Ruby)緊密相關的一類DSL。它基於通用編程語言實現。
例如,Rails框架被稱為基於Ruby的DSL,用於管理Ruby開發的Web應用程序。Rails之所以被稱為DSL,原因之一在於Rails應用了一些Ruby語言的特性,使得基於Rails編程看上去與基於通用目的的Ruby語言編程並不相同。
根據Martin Fowler和Eric Evans的觀點,框架或者程序庫的API是否滿足內部DSL的關鍵特征之一就是它是否有一個流暢(fluent)的接口。這樣,你就能夠用短小的對象表達式去組織一個原本很長的表達式,使它讀起來更加自然。
外部DSL
外部DSL跟通用編程語言(GPL)類似,但是外部DSL更加專注於特定領域。
創建外部DSL和創建一種通用的編程語言的過程是相似的,它可以是編譯型或者解釋型的。它具有形式化的文法,隻允許使用良好定義的關鍵字和表達式類型。經過編譯的DSL通常不會直接產生可執行的程序(但是它確實可以)。
大多數情況下,外部DSL可以轉換為一種與核心應用程序的操作環境相兼容的資源,也可以轉換為用於構建核心應用的通用目的編程語言。例如,Hibernate中使用的對象-關係映射文件,就是由外部DSL轉換為資源的實例。
提示:關於 DSL 的詳細介紹可以參考:《領域特定語言》(Martin Fowler)這本書。
Kotlin 的 DSL 特性支持
許多現代語言為創建內部 DSL 提供了一些先進的方法, Kotlin 也不例外。
在Kotlin 中創建 DSL , 一般主要使用下麵兩個特性:
- 擴展函數、擴展屬性
- 帶接收者的 Lambda 表達式(高階函數)
例如上麵的示例的 UI {...}
的代碼,我們舉例簡單說明如下
函數名 | 函數簽名 | 備注說明 |
---|---|---|
UI | fun Fragment.UI(init: AnkoContext.() -> Unit):AnkoContext | android.support.v4.app.Fragment的擴展函數; 入參 init 是一個帶接收者的函數字麵值, 我們直接傳入的是一個 Lambda 表達式 |
verticalLayout | inline fun ViewManager.verticalLayout(init: _LinearLayout.() -> Unit): LinearLayout | android.view.ViewManager的擴展函數 |
使用kotlinx.html DSL 寫前端代碼
為了加深對 Kotlin DSL 實用性上的理解,我們本節再介紹一個 Kotlin 中關於
HTML 的 DSL: kotlinx.html 。
kotlinx.html 是可在 Web 應用程序中用於構建 HTML 的 DSL。 它可以作為傳統模板係統(例如JSP、FreeMarker等)的替代品。
kotlinx. html 分別提供了kotlinx-html-jvm 和 kotlinx-html-js庫的DSL , 用於在 JVM 和瀏覽器 (或其他 javascript 引擎) 中直接使用 Kotlin 代碼來構建 html, 直接解放了原有的 HTML 標簽式的前端代碼。這樣,我們 也可以使用 Kotlin來先傳統意義上的 HTML 頁麵了。 Kotlin Web 編程將會更加簡單純淨。
提示: 更多關於kotlinx.html的相關內容可以參考它的 Github 地址 :https://github.com/Kotlin/kotlinx.html
要使用 kotlinx.html 首先添加依賴
dependencies {
def kotlinx_html_version = "0.6.3"
compile "org.jetbrains.kotlinx:kotlinx-html-jvm:${kotlinx_html_version}"
compile "org.jetbrains.kotlinx:kotlinx-html-js:${kotlinx_html_version}"
...
}
kotlinx.html 最新版本發布在 https://jcenter.bintray.com/ 倉庫上,所以我們添加一下倉庫的配置
repositories {
maven { url 'https://jitpack.io' }
mavenCentral()
jcenter() // https://jcenter.bintray.com/ 倉庫
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
我們來寫一個極簡百度首頁示例。這個頁麵的前端 HTML 代碼如下:
<!DOCTYPE html>
<html lang=zh-CN>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<title>百度一下</title>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<link href="dsl.css" rel="stylesheet">
<script src="dsl.js"></script>
</head>
<body>
<div >
<div >

</div>
<form >
<input >
<button type="submit" >百度一下</button>
</form>
</div>
</body>
</html>
其中,dsl.css文件內容如下
.ipad {
margin: 10px
}
.center {
text-align: center;
}
dsl.js 文件內容如下
$(function () {
$('#baiduBtn').on('click', function () {
var wd = $('#wd').val()
window.open("https://www.baidu.com/s?wd=" + wd)
})
})
上麵我們是通常使用的 HTML+JS+CSS 的方式來寫前端頁麵的方法。現在我們把 HTML 部分的代碼用Kotlin 的 DSL kotlinx.html 來重新實現一遍。
我們首先新建 Kotlin + Spring Boot 工程,然後直接來寫 Kotlin 視圖類HelloDSLView,代碼如下:
package com.easy.kotlin.chapter14_kotlin_dsl.view
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.springframework.stereotype.Service
/**
* Created by jack on 2017/7/22.
*/
@Service
class HelloDSLView {
fun html(): String {
return createHTML().html {
head {
meta {
charset = "utf-8"
httpEquiv = "X-UA-Compatible"
content = "IE=edge"
}
title("百度一下")
link {
href = "https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
rel = "stylesheet"
}
script {
src = "https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"
}
link {
href = "dsl.css"
rel = "stylesheet"
}
script {
src = "dsl.js"
}
}
body {
div(classes = "container") {
div(classes = "ipad center") {
img {
src = "https://www.baidu.com/img/bd_logo1.png"
width = "270"
height = "129"
}
}
form(classes = "form") {
input(InputType.text, classes = "form-control ipad") {
id = "wd"
}
button(classes = "btn btn-primary form-control ipad") {
id = "baiduBtn"
type = ButtonType.submit
text("百度一下")
}
}
}
}
}
}
}
相比之下,我們使用 DSL 的風格要比原生 HTML 要簡潔優雅。關鍵是,我們的這個 HTML 是用 Kotlin 寫的,這也就意味著,我們的 HTML 代碼不再是簡單的靜態的前端代碼了。我們完全可以直接使用後端的接口返回數據來給 HTML 元素賦值,我們也完全具備了(當然是完全超越了)諸如 JSP、Freemarker 這樣的視圖模板引擎的各種判斷、循環等的語法功能,因為我們直接使用的是一門強大的編程語言 Kotlin 來寫的 HTML 代碼 。
然後,我們就可以直接在控製器層的代碼裏直接調用我們的 Kotlin 視圖代碼了:
@Controller
class HelloDSLController {
@Autowired
var helloDSLView: HelloDSLView? = null
@GetMapping("hello")
fun helloDSL(model: Model): ModelAndView {
model.addAttribute("hello", helloDSLView?.html())
return ModelAndView("hello")
}
}
為了簡單起見,我們借用一下 Freemarker 來做視圖解析引擎,但是它隻負責原封不動地來傳輸我們的 Kotlin 視圖代碼。hello.ftl 代碼如下:
${hello}
我們的源碼目錄如下
── src
├── main
│ ├── java
│ ├── kotlin
│ │ └── com
│ │ └── easy
│ │ └── kotlin
│ │ └── chapter14_kotlin_dsl
│ │ ├── Chapter14KotlinDslApplication.kt
│ │ ├── controller
│ │ │ └── HelloDSLController.kt
│ │ └── view
│ │ └── HelloDSLView.kt
│ └── resources
│ ├── application.properties
│ ├── banner.txt
│ ├── static
│ │ ├── dsl.css
│ │ ├── dsl.js
│ │ └── hello.html
│ └── templates
│ └── hello.ftl
└── test
├── java
├── kotlin
│ └── com
│ └── easy
│ └── kotlin
│ └── chapter14_kotlin_dsl
│ └── Chapter14KotlinDslApplicationTests.kt
└── resources
然後,啟動運行 SpringBoot 應用,瀏覽器訪問 https://127.0.0.1:8888/hello , 我們可以看到如下輸出界麵:
這就是 DSL 的精妙之處。我們後麵可以嚐試使用 kotlinx.html 來寫Kotlin 語言的前端代碼了。在做 Web 開發的時候,我們通常是使用 HTML + 模板引擎(Velocity、JSP、Freemarker 等)來集成前後端的代碼,這讓我們有時候感到很尷尬,要學習模板引擎的語法,還得應對 前端HTML代碼中淩亂的模板引擎標簽、變量等片段代碼。
使用 Kotlin DSL 來寫 HTML 代碼的情況將完全不一樣了,我們將重拾前後端集成編碼的樂趣(不再是模板引擎套前端 HTML,各種奇怪的 #、<#>、${} 模板語言標簽),我們直接把 更加優雅簡單的 DSL 風格的HTML 代碼搬到了後端,同時HTML中的元素將直接跟後端的數據無縫交互,而完成這些的隻是 Kotlin(當然,相應領域的 DSL 基本語義模型還是要學習一下)。
提示:本節項目源碼: https://github.com/EasyKotlin/chapter14_kotlin_dsl
實現一個極簡的http DSL
我們現在已經基本知道 Kotlin 中 DSL 的樣子了。但是這些 DSL 都是怎樣實現的呢?本節我們就通過實現一個極簡的http DSL來學習創建 DSL 背後的基本原理。
在這裏我們對 OkHttp 做一下簡單的封裝,實現一個類似 jquery 中的 Ajax 的 http 請求的DSL。
OkHttp 是一個成熟且強大的網絡庫,在Android源碼中已經使用OkHttp替代原先的HttpURLConnection。很多著名的框架例如Picasso、Retrofit也使用OkHttp作為底層框架。
提示: 更多關於OkHttp 的使用可參考: https://square.github.io/okhttp/
創建 Kotlin Gradle 項目
我們首先使用 IDEA 創建 Kotlin Gradle 項目
然後,在 build.gradle 裏麵配置依賴
compile 'com.github.ReactiveX:RxKotlin:2.1.0'
compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.8.1'
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.35'
其中,RxKotlin是ReactiveX 框架對 Kotlin 語言的支持庫。我們這裏主要用RxKotlin來進行請求回調的異步處理。
我們使用的是 'com.github.ReactiveX:RxKotlin:2.1.0' , 這個庫是在 https://jitpack.io 上,所以我們在repositories配置裏添加 jitpack 倉庫
repositories {
maven { url 'https://jitpack.io' }
...
}
RxKotlin
ReactiveX是Reactive Extensions的縮寫,一般簡寫為Rx,最初是LINQ的一個擴展,由微軟的架構師Erik Meijer領導的團隊開發,在2012年11月開源。
Rx擴展了觀察者模式用於支持數據和事件序列。Rx是一個編程模型,目標是提供一致的編程接口,幫助開發者更方便的處理異步I/O(非阻塞)數據流。
Rx庫支持.NET、JavaScript和C++ 。Rx近幾年越來越流行,現在已經支持幾乎全部的流行編程語言了。一個語言列表如下所示:
Rx的大部分語言庫由ReactiveX這個組織負責維護。Rx 比較流行的庫有RxJava/RxJS/Rx.NET等,當然未來RxKotlin也必將更加流行。
提示: Rx 的社區網站是: https://reactivex.io/ 。 Github 地址:https://github.com/ReactiveX/
Http請求對象封裝類
首先我們設計Http請求對象封裝類如下
class HttpRequestWrapper {
var url: String? = null
var method: String? = null
var body: RequestBody? = null
var timeout: Long = 10
internal var success: (String) -> Unit = {}
internal var fail: (Throwable) -> Unit = {}
fun success(onSuccess: (String) -> Unit) {
success = onSuccess
}
fun error(onError: (Throwable) -> Unit) {
fail = onError
}
}
HttpRequestWrapper的成員變量和函數說明如下表
成員 | 說明 |
---|---|
url | 請求 url |
method | 請求方法,例如 Get、Post 等,不區分大小寫 |
body | 請求頭,為了簡單起見我們直接使用 OkHttp的RequestBody類型 |
timeout | 超時時間ms,我們設置了默認值是10s |
success | 請求成功的函數變量 |
fail | 請求失敗的函數變量 |
fun success(onSuccess: (String) -> Unit) | 請求成功回調函數 |
fun error(onError: (Throwable) -> Unit) | 請求失敗回調函數 |
http 執行引擎
我們直接調用 OkHttp 的 Http 請求 API
private fun call(wrap: HttpRequestWrapper): Response {
var req: Request? = null
when (wrap.method?.toLowerCase()) {
"get" -> req = Request.Builder().url(wrap.url).build()
"post" -> req = Request.Builder().url(wrap.url).post(wrap.body).build()
"put" -> req = Request.Builder().url(wrap.url).put(wrap.body).build()
"delete" -> req = Request.Builder().url(wrap.url).delete(wrap.body).build()
}
val http = OkHttpClient.Builder().connectTimeout(wrap.timeout, TimeUnit.MILLISECONDS).build()
val resp = http.newCall(req).execute()
return resp
}
它返回請求的響應對象Response。
我們在OkHttpClient.Builder().connectTimeout(wrap.timeout, TimeUnit.MILLISECONDS).build()
中設置超時時間的單位是 TimeUnit.MILLISECONDS
。
我們通過wrap.method?.toLowerCase()
處理請求方法的大小寫的兼容。
使用 RxKotlin 完成請求響應的異步處理
我們首先新建一個數據發射源:一個可觀察對象(Observable),作為發射數據用
val sender = Observable.create<Response>({
e ->
e.onNext(call(wrap))
})
其中,e 的類型是 io.reactivex.Emitter
(發射器),它的接口定義是
public interface Emitter<T> {
void onNext(@NonNull T value);
void onError(@NonNull Throwable error);
void onComplete();
}
其方法功能簡單說明如下:
方法 | 功能 |
---|---|
onNext | 發射一個正常值數據(value) |
onError | 發射一個Throwable異常 |
onComplete | 發射一個完成的信號 |
這裏,我們通過調用onNext方法,把 OkHttp 請求之後的響應對象Response 作為正常值發射出去。
然後我們再創建一個數據接收源:一個觀察者(Observer)
val receiver: Observer<Response> = object : Observer<Response> {
override fun onNext(resp: Response) {
wrap.success(resp.body()!!.string())
}
override fun onError(e: Throwable) {
wrap.fail(e)
}
override fun onSubscribe(d: Disposable) {
}
override fun onComplete() {
}
}
receiver 的 onNext 函數接收 sender 發射過來的數據 Response, 然後我們在函數體內,調用這個響應對象,給 wrap.success 回調函數進行相關的賦值操作。同樣的,onError 函數中也執行相應的賦值操作。
最後,通過 subscribe 訂閱函數來綁定 sender 與 receiver 的關聯:
sender.subscribe(receiver)
作為接收數據的 receiver (也就是 觀察者 (Observer) ),對發送數據的 sender (也就是可被觀察對象( Observable)) 所發射的數據或數據序列作出響應。
這種模式可以極大地簡化並發操作,因為它創建了一個處於待命狀態的觀察者,在未來某個時刻響應 sender 的通知,而不需要阻塞等待 sender 發射數據。這個很像協程中的通道編程模型。
DSL主函數 ajax
我們的ajax DSL主函數設計如下:
fun ajax(init: HttpRequestWrapper.() -> Unit) {
val wrap = HttpRequestWrapper()
wrap.init()
doCall(wrap)
}
其中,參數init: HttpRequestWrapper.() -> Unit
是一個帶接收者的函數字麵量,它的類型是init = Function1<com.kotlin.easy.HttpRequestWrapper, kotlin.Unit>
。 HttpRequestWrapper是擴展函數init()
的接收者,點號 .
是擴展函數修飾符。
我們在函數體內直接調用了這個函數字麵量 wrap.init()
。這樣的寫法可能比較難以理解,這個函數字麵量 init 的調用實際上是 init.invoke(wrap)
,就是把傳入 ajax 的函數參數直接傳遞給 wrap 。為了更簡單的理解這個 init 函數的工作原理,我們通過把上麵的 ajax 函數的代碼反編譯成對應的 Java 代碼如下:
public static final void ajax(@NotNull Function1 init) {
Intrinsics.checkParameterIsNotNull(init, "init");
HttpRequestWrapper wrap = new HttpRequestWrapper();
init.invoke(wrap);
doCall(wrap);
}
也就是說,ajax 函數的一個更容易理解的寫法是
fun ajax(init: HttpRequestWrapper.() -> Unit) {
val wrap = HttpRequestWrapper()
init.invoke(wrap)
doCall(wrap)
}
我們在實際應用的時候,可以直接把 init 寫成Lambda 表達式的形式,因為接收者類型HttpRequestWrapper 可以從上下文推斷出來。
我們這樣調用 ajax 函數:
ajax {
url = testUrl
method = "get"
success {
string ->
println(string)
Assert.assertTrue(string.contains("百度一下"))
}
error {
e ->
println(e.message)
}
}
下麵是幾個測試代碼示例:
package com.kotlin.easy
import com.alibaba.fastjson.JSONObject
import okhttp3.MediaType
import okhttp3.RequestBody
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
/**
* Created by jack on 2017/7/23.
*/
@RunWith(JUnit4::class)
class KAjaxTest {
@Test fun testHttpOnSuccess() {
val testUrl = "https://www.baidu.com"
ajax {
url = testUrl
method = "get"
success {
string ->
println(string)
Assert.assertTrue(string.contains("百度一下"))
}
error {
e ->
println(e.message)
}
}
}
@Test fun testHttpOnError() {
val testUrl = "https://www2.baidu.com"
ajax {
url = testUrl
method = "get"
success {
string ->
println(string)
}
error {
e ->
println(e.message)
Assert.assertTrue("connect timed out" == e.message)
}
}
}
@Test fun testHttpPost() {
var json = JSONObject()
json.put("name", "Kotlin DSL Http")
json.put("owner", "Kotlin")
val postBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json.toString())
ajax {
url = "saveArticle"
method = "post"
body = postBody
success {
string ->
println(string)
}
error {
e ->
println(e.message)
}
}
}
@Test fun testLambda() {
val testUrl = "https://www.baidu.com"
val init: HttpRequestWrapper.() -> Unit = {
this.url = testUrl
this.method = "get"
this.success {
string ->
println(string)
Assert.assertTrue(string.contains("百度一下"))
}
this.error {
e ->
println(e.message)
}
}
ajax(init)
}
到這裏,我們已經完成了一個極簡的 Kotlin Ajax DSL。
本節工程源碼: https://github.com/EasyKotlin/chatper14_kotlin_dsl_http
本章小結
相比於Java,Kotlin對函數式編程的支持更加友好。Kotlin 的擴展函數和高階函數(Lambda 表達式),為定義Kotlin DSL提供了核心的特性支持。
使用DSL的代碼風格,可以讓我們的程序更加直觀易懂、簡潔優雅。如果使用Kotlin來開發項目的話,我們完全可以去嚐試一下。
最後更新:2017-09-14 09:33:27