Kotlin类型安全DSL生成器

在本博客中,我们将展示如何使用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函数将setUplambda表达式应用于一个新的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、派生类型 - 矩形和图像

在接下来的步骤中,我们将实现派生类RectangleImage。两种类型都将继承项目类的属性。下图说明了域对象和构建器的继承链。

要使其正常工作,我们首先必须打开ItemItemBuilder类进行集成。域对象如下所示: Rectangle

kotlin 复制代码
class Rectangle: Item() {
    var color = ""
}

Image

kotlin 复制代码
class Iamge: Item() {
    var source = ""
}

构建器实现的想法是,ItemBuilder的私有Item成员变量将由RectangleBuilderImageBuilder替换为RectangleImage。为了访问项目属性,它必须受到保护。

RectangleBuilderImageBuilder都实现其属性的settergetter,并将其直接委托给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"
        }
    }
}

参考

相关推荐
闲暇部落40 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX3 小时前
Android 分区相关介绍
android
大白要努力!4 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee4 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood4 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-7 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen9 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年16 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX17 小时前
kotlin
开发语言·kotlin
建群新人小猿19 小时前
会员等级经验问题
android·开发语言·前端·javascript·php