在本博客中,我们将展示如何使用Kotlin和构建器模式创建强大的DSL(特定领域语言)。首先我们将讨论Kotlin提供的一些功能。之后,我们将讨论我们的例子。在最后一部分中,我们将逐步实现DSL。
一、什么是DSL - 领域特定语言
Martin Fowler指出,领域特定语言(DSL)是一种针对特定类型问题的计算机语言,而不是针对任何类型软件问题的通信语言。它分为外部DSL和内部DSL。外部语言由其他系统提供。例如,对于大多数数据库来说,SQL语言是一种外部DSL。然而,数据库引擎将SQL视为内部DSL。
借助Kotlin核心功能,您可以为复杂的层次结构问题创建内部DSL。
1.1、特定领域语言的用例
其中典型的用例包括HTML、SQL查询、CSS或正则表达式构建器。或者在一般配置文件中。
二、Kotlin - 类型安全DSL构建器
Kotlin提供了一些本机功能来以半声明方式实现安全DSL构建器。通过将命名良好的函数声明为构建器,并与具有某些接收器的函数文字/lambda函数相结合,您可以创建静态类型的构建器。
2.1、作用域函数 - 完美的DSL幻想
Kotlin提供了一个称为"作用域函数"的概念。作用域函数允许在对象上执行一些代码(具有一定的作用域)。我们感兴趣的范围函数称为"apply"。通过"apply"调用的作用域函数主要用于对象配置。上下文对象可用作接收器(this)。返回值时对象本身。
因此它非常适合在我们的构建器配置中使用。
在我们的示例中,您将找到以下代码。它表示ItemBuilder
类的扩展函数。输入参数是带有接收器的函数文字。换句话说,lambda函数获取一个ItemBuilder
实例作为输入参数。
在函数体内,创建了一个新的构建器。之后,通过使用作用域apply
函数调用中的setup
函数来配置构建器。返回类型是构建器本身,随后在addChild
调用中使用。截取的第二个代码说明了如何使用item
扩展功能。
kotlin
fun ItemBuilder.item(setUp: ItemBuilder.() -> Unit) {
var builder = ItemBuilder().apply(setup)
addChild(builder)
}
这是一个Kotlin函数,它接受一个名为setUp
的lambda表达式作为参数。这个lambda表达式的类型是ItemBuilder.() -> Unit
,它表示一个没有参数和返回值类型的函数。这个函数的作用是创建一个新的ItemBuilder
对象,并将其作为一个子元素添加到当前的ItemBuilder
对象中。具体来说,它使用apply
函数将setUp
lambda表达式应用于一个新的ItemBuilder
对象中,然后将这个新对象作为一个子元素添加到当前的ItemBuilder
对象中。这个函数的实现方式可能因语言和库的不同而有所不同,但它的基本思想是将一个复杂的构建过程分解成多个小的步骤,并将每个步骤封装成一个函数。这种方式可以使代码更加模块化和可维护,并且可以避免重复的代码。
arduino
{
// within the scope of a ItemBuilder
// we have access to the extension function "item"
...
item {
// within the scope of the "new" itembuilder
// this code block is the "setup" input argument
}
}
三、示例说明
本Kotlin DSL教程将实现声明性用户类型界面的一些基本功能。此示例的灵感来自Qt QML语言,这是一种强大的类型JSON的声明性语言。然而,我们将仅实现一小部分功能来展示Kotlin类型安全构建器的强大功能。您可以在下面的链接中找到完整QML文档的链接。
3.1、Item - 基本类型
我们的用户界面对象的基本类型将称为"Item"。它提供了元素的几何方面。我们示例中的所有对象都将集成该类。
yaml
// QML
Item {
x: 10
y: 20
width: 30
height: 40
}
ini
// Kotlin
item {
x = 10
y = 20
width = 30
height = 40
}
3.2、派生类型 - 矩形和图像
我们将实现派生类型。矩形类有背景颜色,图像类有源URL字符串。这两个类都集成了项目的属性和功能。
less
// QML
Rectangle {
width: 100
height: 100
color: "red"
}
ini
// Kotlin
rectangle {
width = 100
height = 100
color = "red"
}
less
// QML
Image {
width: 100
height: 100
source: "pics/qtlogo.png"
}
ini
// Kotlin
image {
width = 100
height = 100
source = "pics/qtlogo.png"
}
3.3、元素组成
用户界面可以通过向项目添加项目来构成。您可以添加任何派生类型。这将显示继承链的复杂性,无论是在域模型还是在构建器中。
less
// QML
Item {
width: 100
height: 100
Rectangle {
width: 50
height: 50
color: "red"
}
Image {
width: 100
height: 100
source: "pics/qtlogo.png"
}
}
ini
// Kotlin
item {
width = 100
height = 100
rectangle {
width = 50
height = 50
color = "red"
}
image {
width = 100
height = 100
source = "pics/qtlogo.png"
}
}
四、Kotlin DSL Builder示例实现
4.1、包依赖
下图展示了在Kotlin中实现可靠DSL语言的正确依赖流程。域对象是内圈,不依赖于其他包。构建器包包含所有构建器类。当然,这个包直接依赖于域对象。
特定与域的语言实现是一个额外的层,它使用构建器包中的构建器。事实上,我们将以Kotlin扩展函数的形式在构建器本身上实现DSL功能。这意味着我们将扩展构建器本身的功能,而不是修改它们。
重要的是不要混合这些包,因为它可能会引入循环依赖。此外,最佳实践是首先拥有一个强大且使用的构建器模式,然后专注于DSL。DSL的性能取决于您的域和构建器的性能。请注意,此以来关系图符合RC Martin提出的Clean Architecture。
4.2、项目实施
正如前面所指出的,首先为域对象构建可靠的构建器,然后再转向DSL模型,这一点很重要。代码将逐步修改以考虑新功能。
Item
ini
package domain
class Item {
var x = 0
var y = 0
var width = 0
var height = 0
}
IteBuilder
kotlin
package builder
import domain.Item
class ItemBuilder {
private val item = Item()
var x: Int
get() {
return item.x
}
set(value) {
item.x = value
}
// ... similar for y, width, height
fun build(): Item {
return item
}
}
在Kotlin中创建正确的构建器有多种方法。在我们的版本中,我们决定使用域对象的私有成员变量,并重新创建直接委托给它的公共setter/getter。优点是我们可以明确且完全控制构建器的可公开访问的功能。请注意,在apply
块中,只能访问构建器的公共接口。然而,通电是重复的代码创建。
根据您的情况,您可能会选择略有不同的构建器模式版本。
4.2.1、项目DSL
kotlin
package dsl
import builder.ItemBuilder
import domain.Item
fun item(itemBuilder: ItemBuilder.() -> Unit): Item {
return ItemBuilder.apply(itemBuilder).build()
}
此时该item函数是一个可以在全局范围访问的独立函数。在后面的部分中,我们将看到这可能是有问题的。因此,我们将顶级项目的名称更改为"window"。这将有助于避免对函数的不明确调用item。
Main with Builder
ini
import builder.ItemBuilder
fun main() {
var item = ItemBuilder()
.apply {
x = 10
y = 20
width = 30
height = 40
}
.build()
}
Main with Domain Specific Language
ini
import dsl.item
fun main() {
var item = item {
x = 10
y = 20
width = 30
height = 40
}
}
到了这个阶段,主程序的第二个版本就比第一个版本更加简洁和易于理解。正如您所看到的,第二个版本是一种实用程序函数,它调用构造器。这凸显了首先要有强大的构建器模式的一点。
下图简要显示了程序包的设置方式以及项目的配置方式。
4.3、物品构成
在下一步中,我们将实现如何通过组合项目来创建项目的层次结构。每个项目都有一个子对象列表。所有子项都会知道它们是否有父项以及那个父项。
"父子关系"是由ItemBuilder
处理的,现在它有了一个用于添加项目的新函数。 Item
ini
class Item {
var x = 0
var y = 0
var width = 0
var height = 0
var children = mutableListOf<Item>()
var parent: Item? = null
}
ItemBuilder
kotlin
class ItemBuilder {
...
fun addChild(itemBuilder: ItemBuilder): ItemBuilder {
var newItem = itemBuilder.build()
newItem.parent = item
item.children.add(itemBuilder.build())
return this
}
}
4.3.1、窗口和项目扩展功能
如上所述,我们将free
函数的名称更改为window。此外,我们在ItemBuilder
上创建一个扩展函数,它将使用即将创建的addChild
函数。扩展函数返回void。
kotlin
fun window(itemBuilder: ItemBuilder.() -> Unit): Item {
return ItemBuilder.apply(itemBuilder).build()
}
fun ItemBuilder.item(itemBuilder: ItemBuilder.() -> Unit) {
val builder = ItemBuilder().apply(itemBuilder)
addChild(builder)
}
现在我们可以组合我们的项目了。同样,使用DSL语言的版本更容易阅读。唯一的缺点是几乎创建的"window"功能。 Main with Builder
ini
fun main() {
var item = ItemBuilder()
.apply {
x = 0
y = 10
width = 20
height = 30
}
.addChild(
ItemBuilder.apply {
x = 40
y = 50
width = 60
height = 110
}
)
.addChild(
ItemBuilder().apply {
x = 80
y = 90
width = 100
height = 110
}
)
.build()
}
Main with Domain Specific Language
ini
fun main() {
var item = window {
x = 0
y = 10
width = 20
height = 30
item {
x = 40
y = 50
width = 60
height = 70
}
item {
x = 80
y = 90
width = 100
height = 100
}
}
}
如果我们仔细查看创建的项目,我们可以看到它的配置正确。顶级项目没有父级,但有2个子项。孩子们有正确的父母,但没有自己的孩子。
4.4、派生类型 - 矩形和图像
在接下来的步骤中,我们将实现派生类Rectangle
和Image
。两种类型都将继承项目类的属性。下图说明了域对象和构建器的继承链。
要使其正常工作,我们首先必须打开Item
和ItemBuilder
类进行集成。域对象如下所示: Rectangle
kotlin
class Rectangle: Item() {
var color = ""
}
Image
kotlin
class Iamge: Item() {
var source = ""
}
构建器实现的想法是,ItemBuilder
的私有Item
成员变量将由RectangleBuilder
和ImageBuilder
替换为Rectangle
或Image
。为了访问项目属性,它必须受到保护。
RectangleBuilder
和ImageBuilder
都实现其属性的setter
和getter
,并将其直接委托给Item
成员变量的强制转换版本。
RectangleBuilder
kotlin
class RectangleBuilder: ItemBuilder() {
init {
item = Rectangle()
}
private var rectangle: Rectangle
get() {
return item as Rectangle
}
set(value) {
item = value
}
var color: String
get() {
return rectangle.color
}
set(vallue) {
rectangle.color = value
}
}
ImageBuilder
kotlin
class ImageBuilder: ItemBuilder() {
init {
item = Image()
}
private var image: Image
get() {
return item as Image
}
set(value) {
item = value
}
var source: String
get() {
return image.source
}
set(value) {
iamge.source = value
}
}
通过这些更改,我们现在可以根据需要组合视图项、矩形和图像。 Main with Builders
ini
var item = ItemBuilder()
.apply {
x = 0
y = 10
width = 20
height = 30
addChild(
ImageBuilder()
.apply {
x = 40
y = 50
width = 60
height = 70
source = "url"
}
.addChild(
RectangleBuilder()
.apply {
x = 80
y = 90
width = 100
height = 100
color = "red"
}
)
)
}.build()
Main with Domain Specific Language
ini
var item = window {
x = 0
y = 10
width = 20
height = 30
image {
x = 40
y = 50
width = 60
height = 70
source = "url"
rectangle {
x = 80
y = 90
width = 100
height = 100
color = "red"
}
}
}