干货 | 实现一个属于你的“语言”-携程Kotlin DSL开发与实践

作者简介

刘媛,携程金融高级开发工程师,主要负责中文版、国际版支付Android端的开发及维护工作。

每一个DSL,都是一定意义上专有的语言,这篇文章希望能够用浅显易懂的方式,将Kotlin DSL的应用与实践经验分享给大家。希望对你有所启发,能够构建一门属于自己的专有“语言”。

一、简介

DSL(domain specific language),即领域专用语言:专门解决某一特定问题的计算机语言。由于它是以简洁的形式进行表达,整体上直观易懂,使得调用代码和读代码的成本都得以降低,即使是不懂编程语言的一般人都可以进行使用,所以近年来频频被提起,颇受关注。

DSL分为外部DSL和内部DSL。

DSL:在主程序设计语言之外,用一种单独的语言表示领域专有语言。可以是定制语法,或者遵循另外一种语法,如XML、JSON。

内部DSL:通常是基于通用编程语言实现,具有特定的风格,如 iOS 的依赖管理组件 CocoaPods 和 Android 的主流编译工具 Gradle。

这里主要分享在Kotlin中构建使用DSL。


二、应用

Kotlin DSL的应用广泛,包括gradle编写、编写js、html、SQL等。下面列举几个使用场景:

2.1 Trip.com支付网络封装实践

在编写网络代码时,出现频率最高的就是request配置和大篇幅的response回调处理,那么这两部分的代码该如何优化?在Trip.com支付中利用kotlin DSL对网络进行二次封装,针对以上问题进行解决。

定义request配置,使得最终在做request配置时更为简洁:

fun requestBean(request: () -> BusinessBean) {    payClientBuilder.setRequestBean(request())}fun needRetry(needRetry: () -> Boolean) {    payClientBuilder.setNeedRetry(needRetry())}......

定义回调模版,解决以下问题:部分网络请求,我们不关心结果,或者不关心onFailed的场景,避免掉这部分的冗余代码:

private var callSubSuccess: ((T) -> Unit)? = nullprivate var callSubFailed: ((Client.Error?) -> Unit)? = nullprivate var subCallback: PayNetCallback<T> = object : PayNetCallback<T> {    override fun onSucceed(response: T) {        callSubSuccess?.invoke(response)    }
override fun onFailed(error: Client.Error?) { callSubFailed?.invoke(error) }}fun subSuccess(subSuccess: (T) -> Unit) { callSubSuccess = subSuccess payClientBuilder.setSubCallBack(subCallback)}
fun subFailed(subFailed: (Client.Error?) -> Unit) { callSubFailed = subFailed payClientBuilder.setSubCallBack(subCallback)}

预定义扩展函数

object PayNetworkClient {    fun <T : BusinessBean> init(        costClass: Class<T>,        config: PayClientConfigBuilder<T>.() -> Unit    ): PayClientBuilder.NetworkClient? {        var networkClient: payClientBuilder.NetworkClient?        with(PayClientConfigBuilder(costClass)) {            networkClient = build(config)        }        return networkClient    }}

最终调用

val networkClient = PayNetworkClient.init(BusinessResponse::class.java) {    //配置部分    requestBean { request }    needRetry { false }    cancelOtherSession { "sendGetPayInfo" }    //回调部分,可根据需求添加subSuccess或subFailed    subSuccess { serviceSuccess(it) }    subFailed { serviceFailed(it) }}networkClient?.send()

在定义DSL的过程中需要权衡冗余度、自由度、可扩展性。上面给出的伪代码消除了重复的模版代码,减少代码冗余,同时也做到自由选择配置项,有一定的自由度和可扩展性。

2.2 海外支付SDK DSL构建项目实践

众所周知Android studio中是使用groovy编写gradle脚本,而groovy由于是动态语言,不可避免的存在一个问题,就是代码提示不够智能,我们在使用groovy时往往需要配合文档进行编写;而kotlin是一种静态语言,使用它编写gradle脚本则可以有比较好的智能提示体验。

在Gradle5.0中,官方提供可以选择在项目中生成Groovy或者kotlin DSL构建脚本,并进一步的优化代码自动完成、重构和其他 IDE 辅助功能,为使用Kotlin DSL的 IDE 用户带来了极大的便利。

可见gradle官方也在努力将kotlin DSL推向大家视野中。

在我们最近的海外支付SDK中,采用该种方式构建项目, 部分gradle代码如下:

import org.jetbrains.kotlin.config.KotlinCompilerVersion
plugins { id("com.android.application") kotlin("android") kotlin("android.extensions")}
android { compileSdkVersion(28) defaultConfig { applicationId = "trip.pay.app" minSdkVersion(21) targetSdkVersion(28) versionCode = 1 versionName = "1.0" testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } }}
dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation(kotlin("stdlib-jdk8", KotlinCompilerVersion.VERSION)) implementation("com.android.support:appcompat-v7:28.0.0") implementation("com.android.support.constraint:constraint-layout:1.1.3") implementation(project(":TripPay"))}
repositories { mavenLocal() maven(url = "maven地址")}

可以看到使用kotlin编写和groovy编写区别不大,所以即使我们要将现有工程中的groovy脚本重写为kotlin脚本,工作量也不会过大。

以上种种都表明Kotlin DSL相对于groovy的优势非常明显,那么我们是不是应该立马开始改造现有的项目?


答案是“否”,因为它目前存在一个致命的缺陷,在首次编译项目时比groovy DSL慢很多,大项目中这一点会被放大,所以大家在上手之前需要慎重权衡利弊。

目前我们在海外支付SDK中利用kotlin DSL构建大约在17s,利用groovy DSL构建大约在16s,时间上来说几乎没有区别,所以小型项目推荐尝试使用!

相信在不久的未来kotlin DSL可以解决这个问题,那么利用kotlin DSL构建项目势必会成为趋势。

2.3 Anko

Anko库包括Anko Commons、Anko Layouts、Anko SQLite、Anko Coroutines,这些都是使用kotlin DSL编写,这里主要介绍Anko Layouts。


在写Android布局时,我们都习惯性的使用XML进行编写,但是可以考虑丢下冗长的XML写法,尝试使用Anko Layout来实现。

XML写法:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">
<EditText android:id="@+id/todo_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/main_edit_hint" />
<Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/main_button_text" />
</LinearLayout>

Anko Layout写法:

verticalLayout {    setGravity(Gravity.CENTER_VERTICAL)    editText {        hintResource = R.string.main_edit_hint         }.lparams(width = matchParent, height = wrapContent)    button {        textResource = R.string.main_button_text        onClick {            toast("click!")        }    }.lparams(width = matchParent, height = wrapContent)}

实际上前文提到过,XML本质上也是一种DSL,但是明显使用Anko Layout风格更加简单、也更加灵活。


XML编写后,我们需要findViewById找到控件,再对控件进行操作、赋值;Anko Layout编写过程中,可以在布局中就直接做显示隐藏、赋值操作等,同时这种写法也有类型安全、空安全、代码复用性强的优势。

Anko Layout由于是直接在kt文件中编写控件,那么它相对于xml来说,还有一个优势,即:减少了XML格式的解析过程,从而实现CPU资源和电量的节省。

XML的执行流程:

Anko Layout执行流程:

Anko库实际上是用kotlin对相关类做了一层扩展包装,基于这一点,它的局限性也体现在于会增加包大小,在使用之前可以根据项目评估一下是否适合引入Anko库。


2.4、创建一个自己的DSL

Kotlin DSl的优势这么多,那么如何自定义一个DSL?


kotlin的扩展函数、高阶函数、lambda表达式、中缀调用、invoke 约定和函数小括号省略等特性,使得Kotlin编写DSL尤为顺畅,我们可以使用这些特性来实现自己的“领域特定语言”。这里给一个简单的示例:

定义Trip、Department类

data class Trip(var name: String? = "", var address: String? = "", var departments: List<Department>? = mutableListOf(), var city: List<String>? = mutableListOf(), var culture: String? = "")
data class Department(var name: String = "", var nameEn: String = "")

定义中间类,主要是为了实现直接DSL方式添加department的效果

class TripBuilder {        var name: String? = ""        var address: String? = ""        var departments = mutableListOf<Department>()
fun department(block: DepartmentBuilder.() -> Unit) {// 简单的写法departments.add(DepartmentBuilder().apply(block).build())即可// 演示invoke实现 val departmentBuilder = DepartmentBuilder() block.invoke(departmentBuilder) departments.add(departmentBuilder.build()) }
fun build(): Trip = Trip(name, address, departments) }
class DepartmentBuilder { var name: String = "" var nameEn: String = ""
fun build(): Department = Department(name, nameEn)}

创建trip的DSL写法

fun trip(block: TripBuilder.() -> Unit): Trip = TripBuilder().apply(block).build()

实现中缀culture方法(只为了演示所用,实际上可以直接赋值)

infix fun Trip.culture(culture: String) {    this.culture = culture}

最终调用效果:

val trip = trip {    name = "Trip"    address = "上海市长宁区金钟路968号凌空SOHO"    department {        name = "机票"        nameEn = "flight"    }    department {        name = "酒店"       nameEn = "hotel"    }    department {        name = "火车票"        nameEn = "train"    }}trip culture "Customer、Teamwork、Respect、Integrity、Partner"Log.i("result",trip)

result结果:

Trip(name=Trip, address=上海市长宁区金钟路968号凌空SOHO, departments=[Department(name=机票, nameEn=flight), Department(name=酒店, nameEn=hotel), Department(name=火车票, nameEn=train)], culture=Customer、Teamwork、Respect、Integrity、Partner)

一个简单的Kotlin DSL就这样实现了,通过封装成结构化的 API 达到了直观易懂、最终调用时代码量减少的效果。即使是一个非kotlin开发人员也可以理解以上格式的含义,完成“Trip”对象的配置使用。

三、写在最后

1)Kotlin编写完的DSL整体简洁直观,调用代码和读代码的成本都得以降低,在生产项目中可以稳定使用。

2)DSL是通过简化语言中的元素,降低使用者的负担,使用者需要按照既定的规范进行编写。所以我们需要提供完善使用文档,以保证接入者学习成本降低。

3)在我们编写的DSL应用范围越来越大时,已有DSL往往满足不了现有的需求,我们仍然需要对DSL进行补充,所以在定义自己的DSL时需要评估后期开发维护效率,注意其可扩展性。

【推荐阅读】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值