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"
        }
    }
}

参考

相关推荐
双桥wow4 分钟前
Android Framework开机动画开发
android
yueqc15 小时前
Kotlin 协程 Flow 操作符总结
kotlin·协程·flow
fanged7 小时前
天马G前端的使用
android·游戏
molong93111 小时前
Kotlin 内联函数、高阶函数、扩展函数
android·开发语言·kotlin
叶辞树12 小时前
Android framework调试和AMS等服务调试
android
慕伏白14 小时前
【慕伏白】Android Studio 无线调试配置
android·ide·android studio
低调小一15 小时前
Kuikly 小白拆解系列 · 第1篇|两棵树直调(Kotlin 构建与原生承载)
android·开发语言·kotlin
跟着珅聪学java15 小时前
spring boot 整合 activiti 教程
android·java·spring
川石课堂软件测试16 小时前
全链路Controller压测负载均衡
android·运维·开发语言·python·mysql·adb·负载均衡
2501_9159214317 小时前
iOS 26 电耗监测与优化,耗电问题实战 + 多工具 辅助策略
android·macos·ios·小程序·uni-app·cocoa·iphone