在Kotlin中,如何将扩展方法添加到另一个类,但只在某个上下文中可见?

Jay*_*ard 21 dsl builder kotlin

在Kotlin中,我想将扩展方法添加到类中,例如添加到类中Entity.但我只想Entity在交易中看到这些扩展,否则隐藏.例如,如果我定义这些类和扩展:

interface Entity {}

fun Entity.save() {}
fun Entity.delete() {}

class Transaction {
    fun start() {}
    fun commit() {}
    fun rollback() {}
}
Run Code Online (Sandbox Code Playgroud)

我现在可以叫意外save(),并delete()在任何时候,但我只希望他们可以在后start()一交易后不再commit()rollback()?目前我可以这样做,这是错误的:

someEntity.save()       // DO NOT WANT TO ALLOW HERE
val tx = Transaction()
tx.start()
someEntity.save()       // YES, ALLOW
tx.commit()
someEntity.delete()     // DO NOT WANT TO ALLOW HERE
Run Code Online (Sandbox Code Playgroud)

如何使它们在正确的上下文中显示和消失?

注意: 这个问题是由作者故意编写和回答的(答案问题),因此对于常见问题的Kotlin主题的惯用答案存在于SO中.还要澄清为Kotlin的alphas写的一些非常古老的答案,这些答案对于当前的Kotlin来说是不准确的.其他答案也欢迎,有很多样式如何回答这个问题!

Jay*_*ard 20

基础:

在Kotlin中,我们倾向于使用传递给其他类的lambdas来赋予它们"范围"或者在执行lambda之前和之后发生的行为,包括错误处理.因此,您首先需要更改代码Transaction以提供范围.这是一个修改过的Transaction类:

class Transaction(withinTx: Transaction.() -> Unit) {
    init {
        start()
        try {
            // now call the user code, scoped to this transaction class
            this.withinTx()
            commit()
        }
        catch (ex: Throwable) {
            rollback()
            throw ex
        }

    }
    private fun Transaction.start() { ... }

    fun Entity.save(tx: Transaction) { ... }
    fun Entity.delete(tx: Transaction) { ... }

    fun Transaction.save(entity: Entity) { entity.save(this) }
    fun Transaction.delete(entity: Entity) { entity.delete(this) }

    fun Transaction.commit() { ... }
    fun Transaction.rollback() { ... }
}
Run Code Online (Sandbox Code Playgroud)

这里我们有一个事务,在创建时需要一个lambda来处理事务中的处理,如果没有抛出异常,它会自动提交事务.(Transaction类的构造函数就像一个高阶函数)

我们还将扩展函数移到了Entity内部,Transaction这样如果不在此类的上下文中,这些扩展函数将不会被看到也无法调用.这包括方法commit()rollback()只能从类内部现在所谓的,因为他们现在是类中范围的扩展功能.

由于接收到的lambda是一个扩展函数,Transaction因此它在该类的上下文中运行,因此可以看到扩展.(参见:带接收器的函数文字)

这个旧代码现在无效,编译器给我们一个错误:

fun changePerson(person: Person) {
    person.name = "Fred" 
    person.save() // ERROR: unresolved reference: save()
}
Run Code Online (Sandbox Code Playgroud)

现在你要写代码而不是存在于一个Transaction块中:

fun actsInMovie(actor: Person, film: Movie) {
    Transaction {   // optional parenthesis omitted
        if (actor.winsAwards()) {
            film.addActor(actor)
            save(film)
        } else {
            rollback()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

传入的lambda被推断为扩展函数,Transaction因为它没有正式声明.

要在事务中将一堆这些"动作"链接在一起,只需创建一系列可在事务中使用的扩展函数,例如:

fun Transaction.actsInMovie(actor: Person, film: Movie) {
    film.addActor(actor)
    save(film)
}
Run Code Online (Sandbox Code Playgroud)

创建更多这样的,然后在传递给Transaction的lambda中使用它们...

Transaction { 
   actsInMovie(harrison, starWars)
   actsInMovie(carrie, starWars)
   directsMovie(abrams, starWars)
   rateMovie(starWars, 5)
}
Run Code Online (Sandbox Code Playgroud)

现在回到最初的问题,我们有交易方法和实体方法只出现在正确的时刻.使用lambdas或匿名函数的副作用是我们最终探索关于如何编写代码的新想法.


Jay*_*ard 12

请参阅主题和基础知识的其他答案,这里是更深层次的...

相关高级主题:

我们没有解决您可能遇到的所有问题.很容易使一些扩展函数出现在另一个类的上下文中.但要同时为两件事做这件事并不容易.例如,如果我希望Movie方法addActor()仅在Transaction块内出现,则更难.该addActor()方法不能同时具有两个接收器.所以我们要么有一个接收两个参数的方法,Transaction.addActorToMovie(actor, movie)要么我们需要另一个计划.

一种方法是使用中间对象来扩展系统.现在,以下示例可能是也可能不合理,但它显示了如何仅根据需要进行此额外级别的公开功能.这是代码,我们在其中更改Transaction以实现接口,Transactable以便我们现在可以随时委托给接口.

当我们添加新功能时,我们可以创建Transactable公开这些功能的新实现,并保持临时状态.然后,一个简单的辅助函数可以轻松访问这些隐藏的新类.所有添加都可以在不修改核心原始类的情况下完成.

核心课程:

interface Entity {}

interface Transactable {
    fun Entity.save(tx: Transactable)
    fun Entity.delete(tx: Transactable)

    fun Transactable.commit()
    fun Transactable.rollback()

    fun Transactable.save(entity: Entity) { entity.save(this) }
    fun Transactable.delete(entity: Entity) { entity.save(this) }
}


class Transaction(withinTx: Transactable.() -> Unit) : Transactable {
    init {
        start()
        try {
            withinTx()
            commit()
        } catch (ex: Throwable) {
            rollback()
            throw ex
        }
    }

    private fun start() { ... }

    override fun Entity.save(tx: Transactable) { ... }
    override fun Entity.delete(tx: Transactable) { ... }

    override fun Transactable.commit() { ... }
    override fun Transactable.rollback() { ... }
}


class Person : Entity { ... }
class Movie : Entity { ... }
Run Code Online (Sandbox Code Playgroud)

后来,我们决定添加:

class MovieTransactions(val movie: Movie, 
                        tx: Transactable, 
                        withTx: MovieTransactions.()->Unit): Transactable by tx {
    init {
        this.withTx()
    }

    fun swapActor(originalActor: Person, replacementActor: Person) {
        // `this` is the transaction
        // `movie` is the movie
        movie.removeActor(originalActor)
        movie.addActor(replacementActor)
        save(movie)
    }

    // ...and other complex functions
}

fun Transactable.forMovie(movie: Movie, withTx: MovieTransactions.()->Unit) {
    MovieTransactions(movie, this, withTx)
}
Run Code Online (Sandbox Code Playgroud)

现在使用新功能:

fun castChanges(swaps: Pair<Person, Person>, film: Movie) {
    Transaction {
        forMovie(film) {
            swaps.forEach { 
                // only available here inside forMovie() lambda
                swapActor(it.first, it.second) 
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,Transactable如果您不介意它处于顶层,而不是在类中,并且使包的命名空间变得混乱,那么整个事情可能只是一个顶级扩展函数.

有关使用中间类的其他示例,请参阅:

  • 在Klutter TypeSafe配置模块中,中间对象用于存储"可以对哪个属性"执行操作的状态,因此可以传递它并且还可以更改其他可用方法.config.value("something").asString()(代码链接)
  • 在Klutter Netflix Graph模块中,中间对象用于转换到DSL语法的另一部分connect(node).edge(relation).to(otherNode).(代码的链接)的测试用例在同一模块中表现出更多的用途,包括如何甚至运营商,如get()invoke()仅在上下文中是可用的.