当前位置:首页 > 科技  > 软件

如何使用Kotlin开发DSL?

来源: 责编: 时间:2024-01-08 09:14:00 136观看
导读译者 | 布加迪审校 | 重楼程序员总是在争论哪种语言是最好的。我们曾比较过C和Pascal,但时过境迁。Python与Ruby之争和Java与C#之争早已远去。每种语言有其优缺点。理想情况下,我们希望扩展语言以满足自己的需要。程序

译者 | 布加迪crK28资讯网——每日最新资讯28at.com

审校 | 重楼crK28资讯网——每日最新资讯28at.com

程序员总是在争论哪种语言是最好的。我们曾比较过CPascal,但时过境迁。Python与Ruby之争Java与C#之争远去。每种语言有其优缺点。理想情况下,我们希望扩展语言以满足自己的需要。程序员早就有这样的机会。我们知道元编程(即创建用来创建程序的程序)的不同方式。在C中,连不起眼的宏允许您小的描述生成大代码。然而,这些宏是不可靠的、有限的,表达力不强。现代语言拥有极富表现力的扩展方式其中一种语言是KotlincrK28资讯网——每日最新资讯28at.com

一、领域特定语言的定义

领域特定语言DSL一种专门为特定主题领域开发的语言,与JavaC#和C++等通用语言不同。这意味着它描述主题领域的任务更容易、更方便、更有表现力,但同时它解决日常任务也不方便、不实用,即它不是一种通用语言。作为DSL的一个例子,您可以使用正则表达式语言。正则表达式的主题领域是字符串格式。crK28资讯网——每日最新资讯28at.com

要检查字符串是否符合格式,只需使用支持正则表达式的库就够了crK28资讯网——每日最新资讯28at.com

private boolean isIdentifierOrInteger(String s) { return s.matches("^//s*(//w+//d*|//d+)$"); }

如果您检查字符串是否符合通用语言(比如Java中的指定格式,您将得到以下代码crK28资讯网——每日最新资讯28at.com

private boolean isIdentifierOrInteger(String s) { int index = 0; while (index < s.length() && isSpaceChar(s.charAt(index))) { index++; } if (index == s.length()) { return false; } if (isLetter(s.charAt(index))) { index++; while (index < s.length() && isLetter(s.charAt(index))) index++; while (index < s.length() && isDigit(s.charAt(index))) index++; } else if (Character.isDigit(s.charAt(index))) { while (index < s.length() && isDigit(s.charAt(index))) index++; } return index == s.length(); }

上面的代码比正则表达式更难阅读,更容易出错,更难以变更crK28资讯网——每日最新资讯28at.com

DSL的其他常见例HTMLCSSSQLUMLBPMN后两使用图形符号。不仅开发人员使用DSL,测试人员和非IT专家也使用DSLcrK28资讯网——每日最新资讯28at.com

二、DSL的类型

DSL分为两种类型外部和内部。外部DSL语言有自己的语法,它们不依赖用来实现持的通用编程语言。crK28资讯网——每日最新资讯28at.com

外部DSL的优缺点crK28资讯网——每日最新资讯28at.com

  • 使用不同语言/现成库生成代码
  • 设置语法方面拥有更多选项
  1. 使用专门的工具ANTLRyacclex
  2. 有时很难描述语法
  3. 没有IDE支持,您需要编写插件

内部DSL基于特定的通用编程语言(宿主语言。也就是说,在宿主语言的标准工具的帮助下,创建允许您编写更紧凑的库。Fluent API方法就是一个例子。crK28资讯网——每日最新资讯28at.com

内部DSL优缺点:crK28资讯网——每日最新资讯28at.com

  • 使用宿主语言表达式为基础
  • 很容易将DSL嵌入到宿主语言的代码中,反之亦然
  • 不需要生成代码
  • 可以作为宿主语言的子程序进行调试
  1. 设置语法方面机会有限

三、一个真实的例子

最近,我们公司需要创建DSL。我们的产品已经实现了购买验收功能。该模块是BPM业务流程管理的一个小型引擎。业务流程常以图形方式表示。比如说,下面的BPMN标注显示了一个由执行任务1,然后并行执行任务2和任务3组成的流程。crK28资讯网——每日最新资讯28at.com

crK28资讯网——每日最新资讯28at.com

能够以编程方式创建业务流程对我们来说非常重要,包括动态构建路径、为审批阶段设置执行者、为阶段执行设置截止日期等。为此,我们先尝试使用Fluent API方法来解决这个问题。crK28资讯网——每日最新资讯28at.com

然后我们得出结论使用Fluent API设置验收路径仍然很麻烦,我们的团队考虑了创建自己的DSL这种方案。我们研究了基于Kotlin外部DSL和内部DSL的验收路径是什么样子(因为我们的产品代码是用Java和Kotlin编写的crK28资讯网——每日最新资讯28at.com

外部DSLcrK28资讯网——每日最新资讯28at.com

Acceptance addStep executor: HEAD_OF_DEPARTMENT duration: 7 days protocol should be formed parallel addStep executor: FINANCE_DEPARTMENT or CTO or CEO condition: ${!request.isInternal} duration: 7 work days after start date addStep executor: CTO dueDate: 2022-12-08 08:00 PST can change addStep executor: SECRETARY protocol should be signed内部DSL:acceptance { addStep {  executor = HEAD_OF_DEPARTMENT  duration = days(7)  protocol shouldBe formed } parallel {  addStep {   executor = FINANCE_DEPARTMENT or CTO or CEO   condition = !request.isInternal   duration = startDate() + workDays(7)  }  addStep {   executor = CTO dueDate = "2022-12-08 08:00" timezone PST   +canChange  } } addStep {  executor = SECRETARY  protocol shouldBe signed }}

除了花括号外,这两个选项几乎一样。因此,决定不浪费时间和精力开发外部DSL,而是创建内部DSL。crK28资讯网——每日最新资讯28at.com

四、实施DSL的基本结构

不妨开始开发一个对象模型crK28资讯网——每日最新资讯28at.com

interface AcceptanceElementclass StepContext : AcceptanceElement { lateinit var executor: ExecutorCondition var duration: Duration? = null var dueDate: ZonedDateTime? = null val protocol = Protocol() var condition = true var canChange = ChangePermission()}class AcceptanceContext : AcceptanceElement { val elements = mutableListOf<AcceptanceElement>() fun addStep(init: StepContext.() -> Unit) {  elements += StepContext().apply(init) } fun parallel(init: AcceptanceContext.() -> Unit) {  elements += AcceptanceContext().apply(init) }}object acceptance { operator fun invoke(init: AcceptanceContext.() -> Unit): AcceptanceContext {  val acceptanceContext = AcceptanceContext()  acceptanceContext.init()  return acceptanceContext }}

Lambdas

首先看一下AcceptanceContext类。它旨在用于存储路径元素的集合,并用于表示整个图以及parallel-blocks。addStep和parallel方法接受带有接收者的lambda作为参数。crK28资讯网——每日最新资讯28at.com

带有接收者的lambda是定义可以访问特定接收者对象的lambda表达式的一种方式。在函数主体中,传递给调用的接收者对象变成了隐式的this,这样您就可以在没有任何附加限定符的情况下访问该接收者对象的成员,或者使用this表达式访问接收者对象。crK28资讯网——每日最新资讯28at.com

外,如果方法调用的最后一个参数是lambda,可以将lambda放在括号之外。这就是为什么在DSL中我们可以按如下方式编写代码crK28资讯网——每日最新资讯28at.com

parallel { addStep {  executor = FINANCE_DEPARTMENT  ... } addStep {  executor = CTO  ... }}

这相当于没有语法糖的代码:crK28资讯网——每日最新资讯28at.com

parallel({ this.addStep({  this.executor = FINANCE_DEPARTMENT  ... }) this.addStep({  this.executor = CTO  ... })})

带接收者的Lambda和括号外的Lambda是Kotlin在处理DSL时特别有用的特性。crK28资讯网——每日最新资讯28at.com

对象声明

现在不妨看看实体acceptanceacceptance是一个对象。在Kotlin中,对象声明是定义单例的一种方式,单例指只有一个实例的类。因此,对象声明同时定义了类及其单个实例。crK28资讯网——每日最新资讯28at.com

“invoke”操作符重载

此外,为accreditation对象重载了invoke操作符invoke操作符是一个可以在类中定义的特殊函数。当您像调用函数一样调用类的实例时,调用invoke操作符函数。这允许您将对象作为函数来处理,并以类似函数的方式调用它们。crK28资讯网——每日最新资讯28at.com

注意,invoke方法的参数也是带接收者的lambda。现在我们可以定义验收路径:crK28资讯网——每日最新资讯28at.com

val acceptanceRoute = acceptance { addStep {  executor = HEAD_OF_DEPARTMENT  ... } parallel {  addStep {   executor = FINANCE_DEPARTMENT   ...  }  addStep {   executor = CTO   ...  } } addStep {  executor = SECRETARY  ... }}

然后处理它crK28资讯网——每日最新资讯28at.com

val headOfDepartmentStep = acceptanceRoute.elements[0] as StepContext val parallelBlock = acceptanceRoute.elements[1] as AcceptanceContext val ctoStep = parallelBlock.elements[1] as StepContext

五、添加细节

中缀函数

看看这段代码crK28资讯网——每日最新资讯28at.com

addStep { executor = FINANCE_DEPARTMENT or CTO or CEO ...}

我们可以按以下方式实现这个:crK28资讯网——每日最新资讯28at.com

enum class ExecutorConditionType { EQUALS, OR } data class ExecutorCondition( private val name: String, private val values: Set<ExecutorCondition>, private val type: ExecutorConditionType, ) { infix fun or(another: ExecutorCondition) = ExecutorCondition("or", setOf(this, another), ExecutorConditionType.OR) } val HEAD_OF_DEPARTMENT = ExecutorCondition("HEAD_OF_DEPARTMENT", setOf(), ExecutorConditionType.EQUALS) val FINANCE_DEPARTMENT = ExecutorCondition("FINANCE_DEPARTMENT", setOf(), ExecutorConditionType.EQUALS) val CHIEF = ExecutorCondition("CHIEF", setOf(), ExecutorConditionType.EQUALS) val CTO = ExecutorCondition("CTO", setOf(), ExecutorConditionType.EQUALS) val SECRETARY = ExecutorCondition("SECRETARY", setOf(), ExecutorConditionType.EQUALS) 

ExecutorCondition类允许我们设置几个可能的任务执行器。在ExecutorCondition中定义了中缀函数or。中缀函数是一种特殊的函数,允许您使用更自然的中缀符号来调用它。crK28资讯网——每日最新资讯28at.com

如果不使用语言的这个特性,我们将不得不这样写crK28资讯网——每日最新资讯28at.com

addStep { executor = FINANCE_DEPARTMENT.or(CTO).or(CEO) ... }

中缀函数还用于设置协议的所需状态和时区时间。crK28资讯网——每日最新资讯28at.com

enum class ProtocolState { formed, signed}class Protocol { var state: ProtocolState? = null infix fun shouldBe(state: ProtocolState) { this.state = state }}enum class TimeZone { ... PST, ...}infix fun String.timezone(tz: TimeZone): ZonedDateTime { val format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z") return ZonedDateTime.parse("$this $tz", format)}

扩展函数

String.timezone是一个扩展函数。在Kotlin中,扩展函数允许您向现有类添加新函数,而无需修改它们的源代码。当您想要增强无法控制的类的功能时,比如来自标准库或外部库的类,这项特性特别有用。crK28资讯网——每日最新资讯28at.com

DSL中的用法crK28资讯网——每日最新资讯28at.com

addStep { ... protocol shouldBe formed dueDate = "2022-12-08 08:00" timezone PST ... }

这里的“2022-12-08 08:00”是接收者对象,针对它调用扩展函数timezone,而PST是参数。使用this关键字访问接收者对象。crK28资讯网——每日最新资讯28at.com

操作符重载

我们在DSL中使用的下一个Kotlin特性是操作符重载。我们已经考虑了invoke操作符的重载。在Kotlin中,您可以重载其他操作符,包括算术操作符。crK28资讯网——每日最新资讯28at.com

addStep { ... +canChange}

这里,一元操作符+被重载。下面是实现这个重载的代码
crK28资讯网——每日最新资讯28at.com

class StepContext : AcceptanceElement { ... var canChange = ChangePermission() } data class ChangePermission( var canChange: Boolean = true, ) { operator fun unaryPlus() { canChange = true }operator fun unaryMinus() { canChange = false } }

结语

现在我们可以描述DSL上的验收路径然而,应该保护DSL用户避免可能的错误。比如在当前版本中,以下代码是可以接受的crK28资讯网——每日最新资讯28at.com

val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT duration = days(7) protocol shouldBe signed addStep { executor = FINANCE_DEPARTMENT } } }

addStep中的addStep看起来很奇怪,不是?不妨弄清楚为什么这段代码成功编译而没有出现任何错误。如上所述,acceptance#invoke和AcceptanceContext#addStep方法接受带有接收者lambda作为参数,而接收者对象可以通过this关键字访问所以我们可以像这样重写前面的代码crK28资讯网——每日最新资讯28at.com

val acceptanceRoute = acceptance { this@acceptance.addStep { this@addStep.executor = HEAD_OF_DEPARTMENT this@addStep.duration = days(7) this@addStep.protocol shouldBe signed this@acceptance.addStep { executor = FINANCE_DEPARTMENT } } }

现在可以看到this@acceptance.addStep两次都被调用了。特别是对于这种情况,Kotlin有一个DslMarker注释。您可以使用@DslMarker来定义自定义注释。用相同此类注释标记的接收者无法对方的内部访问。crK28资讯网——每日最新资讯28at.com

@DslMarker annotation class AcceptanceDslMarker @AcceptanceDslMarker class AcceptanceContext : AcceptanceElement { ... } @AcceptanceDslMarker class StepContext : AcceptanceElement { ... }

现在crK28资讯网——每日最新资讯28at.com

val acceptanceRoute = acceptance { addStep {  ... addStep { ... } }}

由于错误'fun addStep(init: StepContext.() -> Unit): Unit'无法通过隐式接收者在此上下文中调用,上面这段代码无法编译。必要时使用显式接收者。crK28资讯网——每日最新资讯28at.com

原文标题:How to Develop a DSL in Kotlin,作者:Fedor YaremenkocrK28资讯网——每日最新资讯28at.com


crK28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-57843-0.html如何使用Kotlin开发DSL?

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 线程池异常黑洞及其防范策略

下一篇: Kafka消息阻塞:拯救面试的八大终极解决方案!

标签:
  • 热门焦点
Top