首页 > 解决方案 > 在 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
     }
}

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

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

错误:'inline fun 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 来访问父作用域。但是我们必须使用另一个合格的 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
        }
    }
    

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

标签: kotlinscopedslbuilder

解决方案


  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 示例

推荐阅读