DSL简介
DSL是什么?
- 所谓的DSL(Domain Specified Language)领域专用语言,其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。以上是我拿官方的回答,是不是比较难懂?
- 其实DSL并不是单独为Kotlin语言提供的,但是作为Android开发者一定使用并且一直在使用DSL,是不是觉得很惊奇,
- 简单来说,通过DSL语言我们可以构建属于自己的语法结构,而在Kotlin中并不只有一种方式实现DSL,而主要的实现方式就是高阶函数,之后会有专栏进行介绍高阶函数
- 下面我们看下build.gradle的代码
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
- Gradle相信各位同学都知道它是基于Groovy的构建工具,上面的代码其实就是Groovy给我们提供的DSL功能。
常见的DSL
常见的DSL在很多领域都能看到,例如:
- 软件构建领域 Ant
- UI 设计师 HTML
- 硬件设计师 VHDL
DSL 与通用编程语言的区别
- DSL 供非程序员使用,供领域专家使用;
- DSL 有更高级的抽象,不涉及类似数据结构的细节;
- DSL表现力有限,其只能描述该领域的模型,而通用编程语言能够描述任意的模型;
DSL分类
根据是否从宿主语言构建而来,DSL 分为:
- 内部 DSL(从一种宿主语言构建而来
- 外部 DSL(从零开始构建的语言,需要实现语法分析器等)
DSL基础用法
接下来,我们来看看DSL在kotlin中如何构建自己的语法
- 首先我们新建一个Dependency,名字可以随便起,然后我们声明一个list数组,为list提供添加的数据的方法,类代码如下:
class Dependency {
var libs = mutableListOf<String>()
fun implementation(lib: String) {
libs.add(lib)
}
}
- 接着我们定义一个高阶函数,参数是Dependency的扩展函数
fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libs
}
以上代码只要你了解高阶函数,肯定可以看得懂,高阶函数中的参数是Dependency的扩展函数,所以我们要先初始化一个Dependency,通过实例调用参数,就可以执行传入的Lambda表达式了,我们新建一个Test.kt,在main方法中使用如下
dependencies {
implementation("com.jacky.ll")
implementation("com.jacky.hh")
}
因为定义的方法,返回的是List可以将它打印出来,代码如下所示:
var list = dependencies {
implementation("com.jacky.ll")
implementation("com.jacky.hh")
}
for (text in list) {
println("$text")
}
- 运行程序,结果如下所示
com.jacky.ll
com.jacky.hh
Process finished with exit code 0
关于了解Android中的Gradle
Groovy简介
- 一种运行在JVM虚拟机上的脚本语言,能够与Java语言无缝结合,如果想了解Groovy可以查看IBM-DeveloperWorks-精通Groovy。
打开Android的build.gradle文件,会看到类似下面的一些语法。
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
通过上面的Android的build.gradle配置文件可以发现,buildscript里有配置了repositories和dependencies,而repositories和dependencies里面又可以配置各自的一些属性。可以看出通过这种形式的配置,我们可以层次分明的看出整个项目构建的一些定制,又由于Android也遵循约定大于配置的设计思想,因此我们仅仅只需修改需要自定义的部分即可轻松个性化构建流程。
Groovy脚本-build.gradle
在Groovy下,我们可以像Python这类脚本语言一样写个脚本文件直接执行而无需像Java那样既要写好Class又要定义main()函数,因为Groovy本身就是一门脚本语言,而Gradle是基于Groovy语言的构建工具,自然也可以轻松通过脚本来执行构建整个项目。作为一个基于Gradle的项目工程,项目结构中的settings.gradle和build.gradle这类xxx.gradle可以理解成是Gradle构建该工程的执行脚本,当我们在键盘上敲出gradle clean aDebug这类命令的时候,Gradle就会去寻找这类文件并按照规则先后读取这些gradle文件并使用Groovy去解析执行。
Groovy语法
要理解build.gradle文件中的这些DSL是如何被解析执行的,需要介绍Groovy的一些语法特点以及一些高级特性,下面从几个方面来介绍Groovy的一些特点。
链式命令
- Groovy的脚本具有链式命令(Command chains)的特性,根据这个特性,当你在Groovy脚本中写出a b c d的时候,Groovy会翻译成a(b).c(d)执行,也就是将b作为a函数的形参调用,然后将d作为形参再次调用返回的实例(Instance)中的c方法。
- 其中当做形参的b和d可以作为一个闭包(Closure)传递过去。例如:
// equivalent to: turn(left).then(right)
turn left then right
// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours
// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good
// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }
Groovy也支持某个方法传入空参数,但需要为该空参数的方法加上圆括号。例如:
// equivalent to: select(all).unique().from(names)
select all unique() from names
如果链式命令(Command chains)的参数是奇数,则最后一个参数会被当成属性值(Property)访问。例如:
// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies
操作符重载
- 有了Groovy的操作符重载(Operator overloading),==会被Groovy转换成equals方法,这样你就可以放心大胆地使用==来比较两个字符串是否相等了,在我们编写gradle脚本的时候也可以尽情使用。
- 关于Groovy的所有操作符重载(Operator overloading)可以查阅:Operator overloading官方教程
委托
委托(DelegatesTo)可以说是Gradle选择Groovy作为DSL执行平台的一个重要因素了。通过委托(DelegatesTo)可以很简单的定制一个控制结构体(Custom control structures),例如下面的代码。
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
接下来可以看下解析上述DSL语言生成的代码。
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
上述转换后的DSL语言,先定义了一个email(Closure)的方法,当执行上述步骤1的时候就会进入该方法内执行,EmailSpec是一个继承了参数中cl闭包里所有方法,比如from、to等等的一个类(Class),通过rehydrate方法将cl拷贝成一份新的实例(Instance)并赋值给code,code实例(Instance),通过rehydrate方法中设置delegate、owner和thisObject的三个属性将cl和email两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl闭包中的from、to等方法会调用到email委托类实例(Instance)中的方法,并可以访问到email中的实例变量(Field)。DELEGATE_ONLY表示闭包(Closure)方法调用只会委托给它的委托者(The delegate of closure),最后使用code()开始执行闭包中的方法。
扩展:DSK还能怎么用
DSL还可以将符合标准API规范的代码转化为符合人类理解的自然语言
这是什么意思?what
- 首先,我们以创建一个用户对象为例,新建User.kt,为了方便打印 我们重写toString方法,代码如下所示:
data class User(var name: String = "", var age: Int = 0) {
override fun toString(): String {
return "My name is $name ,i am $age years old"
}
}
- 仍然在Test.kt中测试代码,下按照API规范我们如何来创建一个User对象
val user = User("jacky", 22)
println(user)
运行结果如下所示:
My name is jacky ,i am 22 years old
Process finished with exit code 0
- 如何使用DSL的方式去创建一个User对象呢,首先我们需要提供一个高阶函数
fun create(block: User.() -> Unit): User {
var user = User()
block(user)
return user
}
定义了一个类型为User扩展函数的高阶函数,通过block调用表达式的部分 所以我们可以直接这样来创建一个User对象:
val user1 = create {
name = "jacky"
age = 22
}
println(user1)
这种方式会更加合理一些,运行结果和上面一致,同学我就不展示出来了
结语
DSL的使用场景远远不止这些,其实前提就是使用好高阶函数,很多例子都讲到了使用DSL来生成HTML的代码,不过在业务中没get到他的作用,想了解的朋友可以私下和我沟通。其实不管任何一种技术,一个框架,我们不能评判他的好坏,存在即合理,推动项目开展才是王道。 好了 ,DSL的基础了解就到这里了。欢迎继续学习