在 Kotlin DSL 构建器中控制范围



我试图找到解决范围问题的完美解决方案,我真的很想听听你的意见。

我有一些无法更改的第三方类:

class Employee {
var id = 0
var name = ""
var card : Card? = null
// ...
}
class Card {
var cardId = 0
}

我的目标是能够建立这样的员工:

val built = employee {
id = 5
name = "max"
addCard {
cardId = 5
}
}

原始豆中没有方法添加卡。 因此,我想出了以下构建器:

@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class Scoped
@Scoped
object Builder {
inline fun employee (init: (@Scoped Employee).() -> Unit): Employee {
val e = Employee()
e.init()
return e
}
inline fun Employee.addCard(init: (@Scoped Card).() -> Unit) {
val c = Card()
c.init()
card = c
}
}

不幸的是,现在我得到臭名昭著的错误:

错误:"内联乐趣 Employee.addCard(init: (Scratch_1.Card).() -> Unit):Unit"不能在此上下文中由隐式接收方调用。如有必要,请使用显式的

我了解错误的原因,我想考虑解决方案。

  1. 删除 DSLMarker 注释以便能够继承父作用域。不幸的是,这允许非法建筑商使用:

    with(Builder) {
    val built = employee {
    id = 5
    name = "max"
    addCard {
    employee {
    // ...
    }
    cardId = 5
    }
    }
    }   
    
  2. 使用限定的 this 访问父范围。但是,我们必须使用另一个合格的方法来获得合适的接收器。这是相当冗长的。

    with(Builder) {
    val built = employee {
    id = 5
    name = "max"
    with(this@with) {
    this@employee.addCard {
    cardId = 5
    }
    }
    }
    }
    
  3. 继承员工以便能够将扩展函数放入其中(这里不可能进行委派,因为我在 Employee 中有很多属性,并且并非所有属性都由接口定义)。 如果第三方类是最终的,则这并不总是有效。

    class EmployeeEx : Employee() {
    inline fun addCard(init: (@Scoped Card).() -> Unit) {
    val c = Card()
    c.init()
    card = c
    }
    }      
    

    和建造者:

    @Scoped
    object Builder {
    inline fun employee (init: (@Scoped EmployeeEx).() -> Unit): Employee {
    val e = EmployeeEx()
    e.init()
    return e
    }
    }
    

那么最好的解决方案是什么?我错过了什么吗? 非常感谢您阅读所有这些!

  1. 您可以定义扩展函数而无需生成新类, 它也适用于外国不可接触的来源:
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class Scoped
@Scoped
object Builder {
inline fun employee(init: (@Scoped Employee).() -> Unit) = Employee().apply(init)
}
fun Employee.addCard(init: (@Scoped Card).() -> Unit) = run { card = Card().apply(init) }
  1. 有两种经典工具可以控制dsl范围:
    • @DSLMarker可实现的代码,因为您正在使用,并且
    • @Deprecated (level = ERROR)第一种方法不起作用的所有其他情况。

例如,目前您可以构建嵌入式员工:

val built = employee {
id = 5
name = "max"
addCard {
cardId = 6
}
employee {  }  // <--- compilable, but does not have sense
}

您可以通过弃用的方式直接禁止此操作:

@Deprecated(level = DeprecationLevel.ERROR, message = "No subcontractors, please.")
fun Employee.employee(init: (@Scoped Employee).() -> Unit) = Unit

现在,以下示例不可编译:

val built = employee {
//...
employee {  }  // <--- compilation error with your message
}
  1. 您可能会发现这很有用:Kotlin DSL 示例

我会提供以下设计,它非常经典,生成的代码很短。

  1. 由于Builder增加了额外的范围并防止了我们丑陋的导入,我们只是通过with构造和重载来停止使用它invoke运算符。

  2. 对可编辑代码使用@DslMarker,对外来代码使用@Deprecated来控制范围

@Scoped
object builder {
operator fun invoke(init: BuildingContext.() -> Unit) = BuildingContext().init()
}
@Scoped
class BuildingContext {
fun employee(init: Employee.() -> Unit) = Employee().apply { init() }
fun Employee.addCard(init: Card.() -> Unit) = run {card = Card().apply { init() }}
@Deprecated(level = DeprecationLevel.ERROR, message = "Employee is forbidden here.")
fun Employee.employee(init: (@Scoped Employee).() -> Unit) { }
@Deprecated(level = DeprecationLevel.ERROR, message = "Card is forbidden here.")
fun Card.addCard(init: (@Scoped Card).() -> Unit) { }
}
fun main(args: Array<String>) {
builder {
val crafted = employee {
//employee {}  <-- deprecated, causes compilation error
id = 5
name = "max"
addCard {
// addCard {} <-- deprecated too
cardId = 7
}
}
println(crafted.card?.cardId)
}
}

完整版在这里工作:https://pl.kotl.in/ICLYZyetU

好的,我想我现在有一个很好的概述。

首先,我认为问题的原因是构建器对象上的IScoped。删除它时,它可以工作。 但是它仍然允许"非法"语法:

val built = employe {
id = 5
name = "max"
addCard {
employe {
}
cardId = 5
}
}

解决方案

只在构建器对象中保留扩展方法,不要在后面放置注释。

就我而言,我必须介绍另一位建筑商才能开始施工

object EmployeBuilder {   
}
object Builder {
inline fun EmployeBuilder.employe(init: (@Scoped Employee).() -> Unit): Employee {
val e = Employee()
e.init()
return e
}
inline fun Employee.addCard(init: (@Scoped Card).() -> Unit) {
val c = Card()
c.init()
card = c
}
}
fun main() {
with(Builder) {
val built = EmployeBuilder.employe {
id = 5
name = "max"
addCard {
cardId = 5
}
}
}
}

现在我们有它:

  1. 没有类上下文的"污染",因为扩展方法仅在构建器对象中可用。
  2. 不能使用非法语法,因为所有参数都使用 DslMarker 注释锁定。

最新更新