第10章 使用 Kotlin 创建 DSL
使用DSL的编程风格,可以让程序更加简单干净、直观简洁。当然,我们也可以创建自己的 DSL。相对于传统的API, DSL 更加富有表现力、更符合人类语言习惯。
10.1 什么是DSL
DSL(Domain-Specific Language,领域特定语言)指的是专注于特定问题领域的计算机语言。不同于通用的计算机语言(GPL),领域特定语言只用在某些特定的领域。
DSL语言能让我们以一种更优雅、更简洁的方式来表达和解决领域问题。之所以能够这样,是因为这个语言刚好够用于这个特定的解决领域中存在的模式,一点儿不多、也一点儿不少,刚刚好。
DSL 简单讲就是对一个特定问题 (受限的表达能力) 的方案模型的更高层次的抽象表达(领域语言),使其更加简单易懂 (容易理解的语义以及清晰的语义模型)。
DSL 只是问题解决方案模型的外部封装,这个模型可能是一个 API 库,也可能是一个完整的框架等等。DSL 提供了思考特定领域问题的模型语言,这使得我们可以更加简单高效地来解决问题。DSL 聚焦一个特定的领域,简单易懂,功能极简但完备。DSL 让我们理解和使用模型更加简易。
比如用来显示网页的HTML语言,在Kotlin 生态中有个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 编程将会更加简单纯净。
更加典型的例子是用于替代 Android 开发中布局 XML文件的 DSL框架 Anko,它使用基于Kotlin 的DSL 来声明Android UI组件,而不是传统的XML。在 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 风格更加简单干净,也更加自由灵活。
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)这本书。
10.2 Kotlin的DSL特性支持
许多现代语言为创建内部 DSL 提供了一些先进的方法, Kotlin 也不例外。
在Kotlin 中创建 DSL , 一般主要使用下面3个特性:
- 扩展函数、扩展属性
- 带接收者的 Lambda 表达式(高阶函数)
- invoke 函数调用约定
例如上面的示例的 UI {...}
的代码,我们举例简单说明如下
函数名 | 函数签名 | 备注说明 |
---|---|---|
UI | fun Fragment.UI(init: AnkoContext<Fragment>.() -> Unit):AnkoContext<T> | android.support.v4.app.Fragment的扩展函数; 入参 init 是一个带接收者的函数字面值, 我们直接传入的是一个 Lambda 表达式 |
verticalLayout | inline fun ViewManager.verticalLayout(init: _LinearLayout.() -> Unit): LinearLayout | android.view.ViewManager的扩展函数 |
关于扩展函数和带接收者的函数字面值我们在前面的章节中已经讲过了。我们这里简单讲一下 Kotlin 中的invoke 操作符函数。
在前面的集合类章节中,我们知道 Kotlin 中使用下标运算符foo[x] 来等价调用 foo.get(x) 操作符函数。同样地,关于invoke 操作符函数调用有一个类似的约定。
我们知道,对一个函数predicate: (T) -> Boolean我们可以直接这样调用predicate(element), 这样的代码实例我们可以在List的扩展函数filter To 中看到
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
在Kotlin中,可以将foo.invoke()简写成foo(),在 kotlin 中操作符是可以重载的,()操作符对应的就是类的重载操作符函数 invoke。即此处的predicate: (T) -> Boolean) 函数的调用
predicate(element)
等价于
predicate.invoke(element)
上面的是函数类型的对象的 invoke 函数的例子。 而实际上在Kotlin 中,在类的对象实例上我们也可以像函数那样直接使用 () 操作符来调用这个类的一个 invoke 操作符函数。用代码示例来说明可能会更加简单直接。一个简单的示例代码如下
>>> class Hello{
... operator fun invoke(name:String){
... println("Hello, $name")
... }
... }
>>> val hello = Hello()
>>> hello("World")
Hello, World
>>> hello("Kotlin")
Hello, Kotlin
这段代码在 Hello 类中定义了一个操作符函数invoke,然后我们声明了一个 Hello 类的实例对象 hello, 接下来神奇的事情发生了
hello("World")
我们直接把这个实例对象 hello 当做函数一样来调用了:给它传入了参数“World”, 在 REPL 中运行上面的代码,我们发现正确输出了
>>> hello("World")
Hello, World
这个特性我们一般情况下在程序代码中很少使用到。但是在 DSL 中,将会非常有用。这个特性会使得我们的 DSL 代码更加简洁清晰。
10.3 实现一个http ajax请求的DSL
OkHttp 是一个成熟且强大的网络库,在Android源码中已经使用OkHttp替代原先的HttpURLConnection。很多著名的框架例如Picasso、Retrofit也使用OkHttp作为底层框架。
我们首先使用 IDEA 创建 Kotlin Gradle 项目
创建 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来进行请求回调的异步处理。
repositories {
maven { url 'https://jitpack.io' }
...
}
RxKotlin
ReactiveX是Reactive Extensions的缩写,一般简写为Rx,最初是LINQ的一个扩展,由微软的架构师Erik Meijer领导的团队开发,在2012年11月开源。
Rx扩展了观察者模式用于支持数据和事件序列。Rx是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步I/O(非阻塞)数据流。
。Rx近几年越来越流行,现在已经支持几乎全部的流行编程语言了。一个语言列表如下所示:
Rx 支持的编程语言 | 项目主页 |
---|---|
Java | : |
JavaScript | : |
C# | : |
C#(Unity) | : |
Scala | : |
Clojure | : |
C++ | : |
Lua | : |
Ruby | : |
Python: | : |
Go | : |
Groovy | : |
JRuby | : |
Kotlin | : |
Swift | : |
PHP | : |
Elixir | : |
Dart | : |
Rx的大部分语言库由ReactiveX这个组织负责维护。Rx 。
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() // 构建OkHttpClient对象
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 =
ajax {
url = testUrl
method = "get"
success {
string ->
println(string)
Assert.assertTrue(string.contains("百度一下"))
}
error {
e ->
println(e.message)
}
}
}
@Test fun testHttpOnError() {
val testUrl =
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 =
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。
本章小结
相比于Java,Kotlin对函数式编程的支持更加友好。Kotlin 的扩展函数和高阶函数(Lambda 表达式),为定义Kotlin DSL提供了核心的特性支持。
使用DSL的代码风格,可以让我们的程序更加直观易懂、简洁优雅。如果使用Kotlin来开发项目的话,我们完全可以去尝试一下。