从 Wiki 开始
领域特定语言(domain-specific language) 指的是专注于某个应用程序领域的计算机语言。不同于普通的跨领域通用计算机语言(GPL),领域特定语言只用在某些特定的领域。
Wiki:「我只想说懂得都懂,不懂的我也不多解释」
散了散了,读完 DSL 的 Wiki 发现解释了个寂寞。事实上网络上解释 DSL 定义的文章很多,但大多数表达的十分抽象。由于这部分不在我们的讨论范围以内,在此不多提及。有兴趣的同学可以自行了解。
DSL 很少有人能讲清楚、听明白,这也是其本身的特点决定的。事实上 DSL 离我们近得很,你可能没听过 DSL,但你一定用过 DSL:SQL、正则表达式。更近一点的:Gradle
让我们从每天都要打交道的 Gradle 开始看起:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 31
defaultConfig {
applicationId "com.devmcry.kotlindsl"
minSdk 23
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
(一个普通的 build.gradle)
当我们在写 build.gradle 的时候,实际上就是在使用 Groovy 语言的内部 DSL 来完成一个 Android 项目的构建。
DSL 表达形式简洁、相对简单的上手、屏蔽了内部的具体实现、这些都大大降低了调用者的学习成本,以及后续人员的维护成本
目前在 Android Studio 创建新项目(Gradle Plugin Version 4.0+)你会发现用了好多年的
build.gradle
不见了,取而代之的是build.gradle.kts
。,你几乎不会感知到切换语言对你后续工作展开所带来的任何困扰。详细信息可查阅 将构建配置从 Groovy 迁移到 KTS 这篇官方文档。
这将是一个漫长的迁移过程,但可以证明 Kotlin DSL 不是花里花哨的小把戏,而是实实在在对开发有所帮助的。现在让 gradle 自己玩会儿,我们再去看看大佬们刚刚分析过的两大框架:Coil、Compose。看过这两份重量级框架的部分源码,相信更能证明我们有必要了解、掌握 DSL,并学习写出这种风格的代码。
Coil、Compose 中的 DSL
imageView.load("https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}
(一段白嫖来的代码)
在加载图片的场景里往往配置项众多,就像上面提到的动画、占位、裁切等等。在 Glide 里使用了链式调用的风格:
Glide.with(context)
.load("https://www.example.com/image.jpg")
.placeholder(R.drawable.image))
...
.into(imageView)
已经足够简便易懂了对吧,但和 Coil 相比还是稍差一筹。随着调用项数量的提升,这种差距也会更加明显。
现在让我们进入 load()
方法看看:
@JvmSynthetic
inline fun ImageView.load(
uri: String?,
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {}
): Disposable = loadAny(uri, imageLoader, builder)
我们要关注的就是这个 builder
。查看 ImageRequest.Builder
内部的代码可以得知这就是一个普通的 builder 模式的产物,那么到底是什么让 Coil 语法风格与 Glide 的不同呢?没错,关键就在于:ImageRequest.Builder.() -> Unit
。
这是一个 lambda,在它的作用域内以 ImageRequest.Builder
作为上下文,lambda 内的调用实际上都是对 ImageRequest.Builder
的内部成员方法、变量的调用。通过 lambda 完成了 builder 的配置后,接下来会在 loadAny()
的最后调用 builder.build()
,这样便使用 DSL 构建了一个完整的图片加载请求。
@JvmSynthetic
inline fun ImageView.loadAny(
data: Any?,
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {}
): Disposable {
val request = ImageRequest.Builder(context)
.data(data)
.target(this)
.apply(builder)
.build()
return imageLoader.enqueue(request)
}
回过头来再看看 Compose,你会发现它也用到了 DSL:
@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}
(Compose 编写 UI)
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
(Compose Column 布局代码)
动手
将你的回调改为 DSL 风格
假设现在有这样一个需求需要将 View 的 TouchEvent 封装,最后对外提供一个各种手势的回调,按照以往的思路应该写这样的一个接口:
interface ViewGestureListener {
// 点击
fun onClick()
// 长按
fun onLongPress()
// 双击
fun onDoubleTap()
// 双指缩放
fun onScale()
// 双指旋转
fun onRotate()
// 拖动
fun onDrag()
}
然后 View
提供一个 setViewGestureListener(listenr: ViewGestureListener)
方法。在识别手势时分别调用以上方法。
然而不同场景下响应的手势大多不同,调用者完全没必要实现每种手势,随着后续手势的不断增加,这酸爽简直了。
这时我们就可以将 ViewGestureListener
改造为 DSL 风格:
inner class ViewGestureAction {
internal var onClickAction: (() -> Unit)? = null
internal var onLongPressAction: (() -> Unit)? = null
internal var onDoubleTapAction: (() -> Unit)? = null
internal var onScaleAction: (() -> Unit)? = null
internal var onRotateAction: (() -> Unit)? = null
internal var onDragAction: (() -> Unit)? = null
fun onClick(action: (() -> Unit)?) {
onClickAction = action
}
fun onLongPress(action: (() -> Unit)?) {
onLongPressAction = action
}
fun onDoubleTap(action: (() -> Unit)?) {
onDoubleTapAction = action
}
fun onScale(action: (() -> Unit)?) {
onScaleAction = action
}
fun onRotate(action: (() -> Unit)?) {
onRotateAction = action
}
fun onDrag(action: (() -> Unit)?) {
onDragAction = action
}
}
View
同样需要提供一个 setViewGestureAction(action: ViewGestureAction.() -> Unit)
方法
fun setViewGestureAction(action: ViewGestureAction.() -> Unit) {
gestureAction = ViewGestureAction().apply(action)
}
最后让我们对比一下调用代码:
// Interface
setViewGestureListener(object : CustomGestureView.ViewGestureListener {
override fun onClick() {
// do nothing
}
override fun onLongPress() {
// do nothing
}
override fun onDoubleTap() {
// do nothing
}
override fun onScale() {
Log.d(TAG, "onScale")
}
override fun onRotate() {
Log.d(TAG, "onRotate")
}
override fun onDrag() {
Log.d(TAG, "onDrag")
}
})
// DSL
setViewGestureAction {
onClick {
Log.d(TAG, "onClick")
}
onLongPress {
Log.d(TAG, "onLongPress")
}
onDoubleTap {
Log.d(TAG, "onDoubleTap")
}
}
更优雅的 DSL
infix 中缀调用
利用 infix 修饰符,可以写出更加 DSL 的代码。
NetDsl init {
okHttp {
cookieJar { TODO() }
applicationInterceptor { TODO() }
applicationInterceptor { TODO() }
networkInterceptor { TODO() }
dns { TODO() }
custom {
connectTimeout(30, TimeUnit.SECONDS)
retryOnConnectionFailure(true)
}
}
retrofit {
baseUrl { "https://www.timeapi.io" }
// callAdapter { TODO() }
converter { GsonConverterFactory.create() }
}
}
infix fun NetDsl.init(block: (NetDsl.() -> Unit)?) {
block?.invoke(this)
}
invoke约定
如果一个类中定义了使用 operator
修饰符的 invoke
方法,就可以被当作函数一样调用该类的实例。
以 setViewGestureAction(action: ViewGestureAction.() -> Unit)
为例:
inner class ViewGestureAction {
// new added
operator fun invoke(action: ViewGestureAction.() -> Unit): ViewGestureAction {
return apply(action)
}
}
fun setViewGestureAction(action: ViewGestureAction.() -> Unit) {
// gestureAction = ViewGestureAction().apply(action)
// 可以改写为
gestureAction = ViewGestureAction()(action)
}
乍看起来意义不大,但在复杂的 DSL 中是很有必要的。不然就等着被遍地都是的 invoke
教育吧。
尾巴
我们介绍了 DSL 风格的赋值以及回调写法,可以看出核心思路与 builder 基本一致。本文讲述的不深,给人一种都是些语法糖级别的内容,后面我应该会写几个 DSL 风格封装的库。真正展现 DSL 写法迷人的地方。