首页 > 其他分享 >第10章 使用 Kotlin 创建 DSL

第10章 使用 Kotlin 创建 DSL

时间:2022-11-30 11:05:24浏览次数:56  
标签:10 函数 Kotlin DSL https wrap com


第10章 使用 Kotlin 创建 DSL

使用DSL的编程风格,可以让程序更加简单干净、直观简洁。当然,我们也可以创建自己的 DSL。相对于传统的API, DSL 更加富有表现力、更符合人类语言习惯。

本章就让我们一起来学习一下 Kotlin中 DSL的相关内容。我们会首先介绍什么是DSL , 然后简单介绍 Kotlin DSL 设计中的特性支持,最后给出了一个http ajax请求的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 编程将会更加简单纯净。

提示: 更多关于kotlinx.html的相关内容可以参考它的 Github 地址 :​​https://github.com/Kotlin/kotlinx.html​

更加典型的例子是用于替代 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

我们现在已经基本知道 Kotlin 中 DSL 的样子了。但是这些 DSL 都是怎样实现的呢?本节我们就通过实现一个极简的http DSL来学习创建 DSL 背后的基本原理。

在这里我们对 OkHttp 做一下简单的封装,实现一个类似 jquery 中的 Ajax 的 http 请求的DSL。

OkHttp 是一个成熟且强大的网络库,在Android源码中已经使用OkHttp替代原先的HttpURLConnection。很多著名的框架例如Picasso、Retrofit也使用OkHttp作为底层框架。

提示: 更多关于OkHttp 的使用可参考: ​​http://square.github.io/okhttp/​

我们首先使用 IDEA 创建 Kotlin Gradle 项目





第10章 使用 Kotlin 创建 DSL_html


创建 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 支持的编程语言

项目主页

Java

​RxJava​​​ : ​​https://github.com/ReactiveX/RxJava​

JavaScript

​RxJS​​​:​​https://github.com/ReactiveX/rxjs​

C#

​Rx.NET​​​:​​https://github.com/Reactive-Extensions/Rx.NET​

C#(Unity)

​UniRx​​​:​​https://github.com/neuecc/UniRx​

Scala

​RxScala​​​:​​https://github.com/ReactiveX/RxScala​

Clojure

​RxClojure​​​:​​https://github.com/ReactiveX/RxClojure​

C++

​RxCpp​​​:​​https://github.com/Reactive-Extensions/RxCpp​

Lua

​RxLua​​​:​​https://github.com/bjornbytes/RxLua​

Ruby

​Rx.rb​​​:​​https://github.com/Reactive-Extensions/Rx.rb​

Python:

​RxPY​​​:​​https://github.com/ReactiveX/RxPY​

Go

​RxGo​​​:​​https://github.com/ReactiveX/RxGo​

Groovy

​RxGroovy​​​:​​https://github.com/ReactiveX/RxGroovy​

JRuby

​RxJRuby​​​:​​https://github.com/ReactiveX/RxJRuby​

Kotlin

​RxKotlin​​​:​​https://github.com/ReactiveX/RxKotlin​

Swift

​RxSwift​​​:​​https://github.com/kzaher/RxSwift​

PHP

​RxPHP​​​:​​https://github.com/ReactiveX/RxPHP​

Elixir

​reaxive​​​:​​https://github.com/alfert/reaxive​

Dart

​RxDart​​​:​​https://github.com/ReactiveX/rxdart​

Rx的大部分语言库由ReactiveX这个组织负责维护。Rx 比较流行的库有RxJava/RxJS/Rx.NET等,当然未来RxKotlin也必将更加流行。

提示: Rx 的社区网站是: ​​http://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 = one rror
}
}

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 one rror(@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 one rror(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来开发项目的话,我们完全可以去尝试一下。

标签:10,函数,Kotlin,DSL,https,wrap,com
From: https://blog.51cto.com/u_15236724/5897834

相关文章

  • 第3章 类型系统与可空类型 《Kotlin项目实战开发》
    跟Java、C和C++一样,Kotlin也是“静态类型编程语言”。通常,编程语言中的类型系统中定义了l 如何将数值和表达式归为不同的类型l 如何操作这些类型l 这些类型之间如......
  • 《Kotlin 程序设计》第七章 Kotlin 编译过程分析
    第七章Kotlin编译过程分析​​http://mp.weixin.qq.com/s/lEFRH523W7aNWUO1QE6ULQ​​我们知道,Kotlin基于Java虚拟机(JVM),通过Kotlin编译器生成的JVM字节码与Java编译的字节......
  • 2010年10月全球web服务器调查
    根据netcraft的调查,2010年10月,对全球232,839,963个网站的web服务器进行了统计,这个月,netcraft在所有域名商观察到有1400万个新站点 如下图,A.........
  • 题解 CF1091C
    题解CF1091C这个题乍一看,好像有点像约瑟夫问题,但是写完了之后会发现,就会发现TLE了因为\(n\le10^9\),而且用约瑟夫问题写的话每次都会跳k步,肯定会超时超时代码这里......
  • 在 win11 下搭建并使用 ubuntu 子系统(同时测试 win10)——(附带深度学习环境搭建)
    对于一个深度学习从事者来说,Windows训练模型有着诸多不便,还好现在Windows的Ubuntu子系统逐渐完善,近期由于工作需求,配置了Windows的工作站,为了方便起见,搭建了Ubuntu子系......
  • 题解 CF1080B
    题解CF1080B莫名就卡到了最优解第一,但是代码又长又臭,很明显我代码实现能力太弱了。。。直接开始讲,我都不知道怎么讲分情况讨论如果\(l=r\):我们只需要考虑这个位置......
  • kx-000010-顺序表-表尾追加元素
    顺序表结构体定义。具体的结构体定义请查看头文件:https://www.cnblogs.com/kxwslmsps/p/16937235.htmltypedefstatusint;//定义函数结果状态typedefintetyp......
  • windows10iis虚拟目录怎么设置
    https://jingyan.baidu.com/article/aa6a2c14f313114c4d19c41c.html打开开始菜单,在WINDOWS管理工具下打开IIS。 打开IIS管理窗口,在左侧的目录树中一直展开到......
  • 代码随想录算法训练营Day11|20. 有效的括号、1047. 删除字符串中的所有相邻重复项、15
    代码随想录算法训练营Day11|20.有效的括号、1047.删除字符串中的所有相邻重复项、150.逆波兰表达式求值20.有效的括号题目链接:20.有效的括号字符串只包括'(',')','{......
  • 10种经典排序算法的JavaScript实现方法
    排序算法是《数据结构与算法》中最基本的算法之一。常见的一些排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。其中,冒泡排序......