查看原文
其他

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

刘媛 携程技术中心 2019-11-26

作者简介

刘媛,携程金融高级开发工程师,主要负责中文版、国际版支付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时需要评估后期开发维护效率,注意其可扩展性。


【推荐阅读】



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存