在 Kotlin 中,我如何为另一个类添加扩展方法,但只在特定上下文中可见?

23
在Kotlin中,我想为一个类添加扩展方法,例如Entity类。但是,我只想在Entity处于事务中时看到这些扩展方法,否则它们将被隐藏。例如,如果我定义这些类和扩展:
interface Entity {}

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

class Transaction {
    fun start() {}
    fun commit() {}
    fun rollback() {}
}

我现在可能会在任何时候意外调用 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

我该如何使它们在正确的上下文中出现和消失?
注意:本问题是由作者(自问自答问题)有意编写并回答的,以便在SO中提供对常见Kotlin主题的惯用答案。还为了澄清一些针对Kotlin旧版本的非常准确的答案。其他答案也是可以接受的,有许多回答方式!

1
好问题!但为什么有两个答案? - voddan
2
我写了一个,然后又写了另一个。 - Jayson Minard
2个回答

23

基础知识:

在Kotlin中,我们倾向于将lambda传递到其他类中,以便为它们提供“范围”或在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() { ... }
}

在这里,我们有一个交易,创建时需要一个lambda来处理事务内的处理,如果没有抛出异常,则自动提交事务。(Transaction类的构造函数就像Higher-Order Function一样)
我们还将Entity的扩展函数移动到Transaction中,以便这些扩展函数不会在不在此类上下文中时被看到或调用。这包括commit()rollback()方法,现在只能从类本身中调用它们,因为它们是作为类范围内的扩展函数。
由于接收到的lambda是Transaction的扩展函数,因此它在该类的上下文中运行,并因此看到了扩展功能。(参见:Function Literals with Receiver
这个旧代码现在已经无效了,编译器给我们一个错误:
fun changePerson(person: Person) {
    person.name = "Fred" 
    person.save() // ERROR: unresolved reference: save()
}

现在,您需要编写的代码将存在于一个Transaction块中:

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

传递进来的Lambda被推断为是一个扩展函数,作用于Transaction上,因为它没有正式声明。
要在事务中链接一系列这些“操作”,只需创建一系列可以在事务中使用的扩展函数,例如:
fun Transaction.actsInMovie(actor: Person, film: Movie) {
    film.addActor(actor)
    save(film)
}

创建更多类似这样的内容,然后在传递给事务的 Lambda 中使用它们...
Transaction { 
   actsInMovie(harrison, starWars)
   actsInMovie(carrie, starWars)
   directsMovie(abrams, starWars)
   rateMovie(starWars, 5)
}

现在回到最初的问题,我们只有在正确的时间出现交易方法和实体方法。使用lambda或匿名函数的副作用是,我们开始探索有关代码组成方式的新思路。

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

后来,我们决定添加:

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

现在使用新功能:

fun castChanges(swaps: Pair<Person, Person>, film: Movie) {
    Transaction {
        forMovie(film) {
            swaps.forEach { 
                // only available here inside forMovie() lambda
                swapActor(it.first, it.second) 
            }
        }
    }
}
如果你不介意将其放在顶级位置、而不是在类中,并且不介意它会在包的命名空间中占用位置,那么这整个过程本来可以只作为一个顶级扩展函数实现在Transactable上。其他使用中间类的示例,请参见:
  • 在Klutter TypeSafe配置模块中,中间对象用于存储“哪个属性”可以被操作的状态,因此可以传递它并且也更改了其他方法的可用性。config.value("something").asString() (代码链接)
  • 在Klutter Netflix Graph模块中,中间对象用于过渡到DSL语法的另一部分connect(node).edge(relation).to(otherNode)。(代码链接)同一模块中的测试案例展示了更多用例,包括即使在上下文中也仅可用的操作符,例如get()invoke()

听起来几乎像是GoF状态模式。 :) 也许你可以从中提取出一个Kotlin的模式。 - Bruno Santos
很久没有听说过GoF了。 - Jayson Minard
老问题,但是非常好的答案!我正在尝试解决一个问题,并且已经通过类似这个解决方案的东西找到了答案。你知道其他的例子/文章吗?我甚至不知道这个叫什么名字。 - Preston Garno
@PrestonGarno 你可能想查找有关Kotlin的DSL Builder文章和主题。这是最可能讨论这些话题的领域。 - Jayson Minard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接