Kotlin为开发人员提供了大量的语言功能,使开发人员更加可读,更简洁。我们可以用这些功能做的一件很酷的事情就是设计一个表达性的领域专用语言或DSL
什么是领域专用语言(domain specific language / DSL)?
首先,DSL究竟是什么,我们为什么要使用它们?让我们来看看维基百科对DSL的定义:
领域专用语言(DSL)是专用于一个特定的应用领域的计算机语言。这与通用语言(GPL)形成对比,通用语言(GPL)广泛适用于各个领域。
基本上,DSL是一种专注于应用程序某一特定部分的语言。另一方面,通用语言(如Kotlin或Java)可用于一个应用程序的多个部分。有几种我们已经熟悉的特定于领域的语言,例如SQL。如果我们看一下SQL中的一个语句,我们注意到它几乎就像一个英文句子,使得它具有表达性和可读性:
SELECT Person.name,Person.age FROM Person ORDER BY Person.age DESC
没有具体的标准来区分DSL和普通的API,但大多数时候我们看到一个区别:使用某种结构或语法。这使得代码更易于理解,不仅对于开发人员而且对于技术含量较低的人员来说也更易于理解。
DSL与Kotlin
现在,我们如何利用Kotlin的语言特性来创建DSL,并为我们带来了哪些优势?
当我们使用另一种通用编程语言(如Kotlin)创建DSL时,我们实际上正在讨论内部DSL。我们没有创建独立的语法,但是我们正在设置一个使用现有语言的特定方法。这给了我们使用我们已经知道的代码的优点,并允许我们将其他Kotlin语句(如for循环)添加到我们的DSL。
除此之外,Kotlin提供了几种方法来创建更清晰的语法,避免使用太多不必要的符号。在本文,我们将重点介绍三个特定功能:
- 在方法括号外使用
Lambda
- 带接受者的
Lambda
- 扩展函数
我们继续这些例子的时候,这些的使用会在一分钟内变得更加清晰。
为了使一切都可以理解,我将使用一个简单的模型来创建我们的DSL。我们不应该在创建任何类的时候创建DSL。这将是一个不必要的工作。使用DSL的好地方可以是配置类或者库接口,用户不需要知道模型。
来写我们的第一个DSL
在这部分我们将创建一个简单的DSL,它能够实例化Person类的一个对象。请注意,这仅仅是一个例子。以下是本教程结束时的一个示例。
val person = person {
name = "John"
age = 25
address {
street = "Main Street"
number = 42
city = "London"
}
}
正如你所看到的,上面的代码是非常容易理解的。即使没有开发者经验的人也可以阅读,甚至做出调整。为了了解我们如何到达那里,我们将采取几个步骤。这是我们要准备的模型:
data class Person(var name: String? = null,
var age: Int? = null,
var address: Address? = null)
data class Address(var street: String? = null,
var number: Int? = null,
var city: String? = null)
很显然,这不是最后想要的优雅模型。最终我们希望拥有不可变的vals
。我们将在后续部分中介绍这一点。
现在要做的第一件事就是创建一个新文件,将保持DSL与模型中的实际类分离。首先为Person类创建一些构造函数。看看我们想要的结果,看到Person的属性是在代码块中定义的。这些花括号实际上是定义一个lambda。这就是使用上面提到的三种Kotlin语言特征中的第一种语言特征的地方:在方法括号外使用Lambda
。
如果一个函数的最后一个参数是一个lambda,可以把它放在方法括号之外。而当你只有一个lambda作为参数时,你可以省略整个括号。person {…}
实际上与person({…})
相同。这在我们的DSL中变得更简洁。现在来编写person函数的第一个版本。
fun person(block: (Person) -> Unit): Person {
val p = Person()
block(p)
return p
}
所以在这里我们有一个创建一个Person对象的函数。它需要一个带有我们在第2行创建的对象的lambda。当在第3行执行这个lambda时,我们期望在返回第4行的对象之前,该对象获得它所需要的属性。下面展示如何使用这个函数:
val person = person {
it.name = "John"
it.age = 25
}
由于这个lambda只接收一个参数,可以用它来调用person对象。这看起来不错,但还不够完美,如果在我们的DSL看到的东西。特别是当我们要在那里添加额外的对象层。这带来了我们接下来提到的Kotlin功能:带接受者的Lambda
。
在person函数的定义中,可以给lambda
添加一个接收者。这样只能在lambda
中访问那个接收器的函数。由于lambda中的函数在接收者的范围内,则可以简单地在接收者上执行lambda,而不是将其作为参数提供。
fun person(block: Person.() -> Unit): Person {
val p = Person()
p.block()
return p
}
这实际上可以通过使用Kotlin提供的apply函数在一个简单的单行程中重写。
fun person(block: Person.() -> Unit): Person = Person().apply(block)
现在可以将其从DSL中删除:
val person = person {
name = "John"
age = 25
}
到目前为止,还差一个Address类,在我们想要的结果中,它看起来很像刚刚创建的person函数。唯一的区别是必须将它分配给Person对象的Address属性。为此,可以使用上面提到的三个Kotlin语言功能中的最后一个:扩展函数。
扩展函数能够向类中添加函数,而无需访问类本身的源代码。这是创建Address对象的完美选择,并直接将其分配给Person的地址属性。这是Dsl文件的最终版本:
fun person(block: Person.() -> Unit): Person = Person().apply(block)
fun Person.address(block: Address.() -> Unit) {
address = Address().apply(block)
}
现在为Person添加一个地址函数,它接受一个Address作为接收者的lambda表达式,就像对person构造函数所做的那样。然后它将创建的Address对象设置为Person的属性:
val person = person {
name = "John"
age = 25
address {
street = "Main Street"
number = 42
city = "London"
}
}
创建dsl其实也就是这么简单的