Kotlin上的DSL:如何用一晚创建自己的编程语言

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

什么是DSL,为什么你需要它?

\n\n

想象一下,你写的不是代码,而是一份用易懂语言编写的指令,它自己就能变成可运行的程序。这正是领域特定语言(DSL — Domain Specific Language)的工作方式。与通用语言(Java、Python、C#)不同,DSL是为解决一个特定任务而创建的——无论是服务器配置、业务规则描述,还是构建复杂的SQL查询。

\n\n

Kotlin是创建DSL的理想候选者。得益于带接收者的Lambda、扩展和类型安全构建器,你可以编写看起来像原生语法的代码。在本文中,我们不仅会讨论理论,还会为测试框架创建一个真实的DSL——你将看到这有多么简单和有趣。

\n\n

为什么Kotlin是DSL之王?

\n\n

Kotlin赢得开发者喜爱不仅是因为与Java的兼容性。它的语法能力允许创建内部DSL——这些语言寄宿在宿主语言内部并使用其语法。以下是支撑任何Kotlin DSL的三大支柱:

\n\n
    \n
  • 带接收者的Lambda——允许无需显式指定名称即可访问对象的方法。这是构建器模式的基础。
  • \n
  • 扩展——为任何类添加新函数,即使你无法修改该类。
  • \n
  • 中缀函数——允许以自然语言风格编写代码:should be equalTo 5。
  • \n
\n\n

这些能力将编写DSL变成了乐高积木:你拿起现成的模块——Lambda、函数、属性——然后用它们组装出易懂且安全的语言。

\n\n

逐步创建用于测试的简单DSL

\n\n

让我们编写一个用于编写测试的DSL。目标是让测试看起来像可读的描述,而不是一组assert调用:

\n\n
test("计算器应该能相加数字") {\n    val result = Calculator.add(2, 3)\n    result should beEqualTo 5\n    result shouldNot beNull\n}
\n\n

你同意吧,这几乎是英语了。现在我们来分析它是如何工作的。

\n\n

步骤1. 创建测试上下文

\n\n

我们需要一个test函数,它接受一个名称和一个代码块。该块在一个上下文中执行,其中should和shouldNot可用:

\n\n
class TestContext {\n    fun Any.should(matcher: Matcher) {\n        if (!matcher.matches(this)) {\n            throw AssertionError("期望 ${matcher.expected},但得到 $this")\n        }\n    }\n\n    fun Any.shouldNot(matcher: Matcher) {\n        if (matcher.matches(this)) {\n            throw AssertionError("期望不 ${matcher.expected},但得到 $this")\n        }\n    }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n    println("运行测试: $name")\n    try {\n        TestContext().block()\n        println("✓ 通过")\n    } catch (e: AssertionError) {\n        println("✗ 失败: ${e.message}")\n    }\n}
\n\n

关键点:TestContext.() -> Unit是一个带接收者的Lambda。在块内部,this引用TestContext对象,因此should和shouldNot方法隐式可用。

\n\n

步骤2. 实现匹配器

\n\n

现在创建简单的匹配器。beEqualTo和beNull是返回Matcher对象的函数:

\n\n
interface Matcher {\n    val expected: String\n    fun matches(value: Any?): Boolean\n}\n\nfun beEqualTo(expected: Any) = object : Matcher {\n    override val expected = "等于 $expected"\n    override fun matches(value: Any?) = value == expected\n}\n\nval beNull = object : Matcher {\n    override val expected = "null"\n    override fun matches(value: Any?) = value == null\n}
\n\n

博客

书籍推荐