In Kotlin, how do I add extension methods to another class, but only visible in a certain context?

前端 未结 2 1570
悲&欢浪女
悲&欢浪女 2020-12-05 05:21

In Kotlin, I want to add extension methods to a class, for example to class Entity. But I only want to see these extensions when Entity is within

2条回答
  •  失恋的感觉
    2020-12-05 05:39

    See the other answer for the main topic and the basics, here be deeper waters...

    Related advanced topics:

    We do not solve everything you might run into here. It is easy to make some extension function appear in the context of another class. But it isn't so easy to make this work for two things at the same time. For example, if I wanted the Movie method addActor() to only appear while inside a Transaction block, it is more difficult. The addActor() method cannot have two receivers at the same time. So we either have a method that receives two parameters Transaction.addActorToMovie(actor, movie) or we need another plan.

    One way to do this is to use intermediary objects by which we can extend the system. Now, the following example may or may not be sensible, but it shows how to go this extra level of exposing functions only as desired. Here is the code, where we change Transaction to implement an interface Transactable so that we can now delegate to the interface whenever we want.

    When we add new functionality we can create new implementations of Transactable that expose these functions and also holds temporary state. Then a simple helper function can make it easy to access these hidden new classes. All additions can be done without modifying the core original classes.

    Core classes:

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

    Later, we decide to add:

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

    Now using the new functionality:

    fun castChanges(swaps: Pair, film: Movie) {
        Transaction {
            forMovie(film) {
                swaps.forEach { 
                    // only available here inside forMovie() lambda
                    swapActor(it.first, it.second) 
                }
            }
        }
    }
    

    Or this whole thing could just have been a top level extension function on Transactable if you didn't mind it being at the top level, not in a class, and cluttering up the namespace of the package.

    For other examples of using intermediary classes, see:

    • in Klutter TypeSafe config module, an intermediary object is used to store the state of "which property" can be acted upon, so it can be passed around and also changes what other methods are available. config.value("something").asString() (code link)
    • in Klutter Netflix Graph module, an intermediary object is used to transition to another part of the DSL grammar connect(node).edge(relation).to(otherNode). (code link) The test cases in the same module show more uses including how even operators such as get() and invoke() are available only in context.

提交回复
热议问题