在Kotlin DSL构建器中控制范围

时间:2019-04-15 07:42:20

标签: kotlin scope dsl builder

我试图为我的示波器问题找到完美的解决方案,我真的很希望您的意见。

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

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

原始bean中没有方法 addCard 。 因此,我想到了以下构建器:

@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(ini​​t:(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
        }
    }
    

那么最好的解决方案是什么?我想念什么吗? 非常感谢您阅读所有这些内容!

3 个答案:

答案 0 :(得分:1)

  1. 您可以定义extension function,而无需获取新类, 它也适用于外国不可接触的来源:
@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 example

答案 1 :(得分:0)

我将提供以下设计,它非常经典,并且代码简短。

  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

答案 2 :(得分:0)

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

首先,我认为问题的原因是构建器对象上的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批注锁定。