如何用kotlin写DSLs(入门篇)

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提供了几种方法来创建更清晰的语法,避免使用太多不必要的符号。在本文,我们将重点介绍三个特定功能:

  1. 在方法括号外使用Lambda
  2. 带接受者的Lambda
  3. 扩展函数

我们继续这些例子的时候,这些的使用会在一分钟内变得更加清晰。

为了使一切都可以理解,我将使用一个简单的模型来创建我们的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其实也就是这么简单的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容