Подтвердить что ты не робот

В Kotlin, как мне добавить методы расширения в другой класс, но только видимый в определенном контексте?

В 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. Также прояснить некоторые действительно старые ответы, написанные для альфов Котлина, которые не точны для сегодняшнего дня Котлин. Другие ответы также приветствуются, есть много стилей, как ответить на это!

4b9b3361

Ответ 1

Основы:

В Котлин мы склонны использовать lambdas, прошедшие в другие классы, чтобы дать им "область" или иметь поведение, которое происходит до и после выполнения лямбда, включая обработку ошибок. Поэтому вам сначала нужно изменить код для 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() { ... }
}

Здесь у нас есть транзакция, которая при создании требует лямбда, которая выполняет обработку внутри транзакции, если исключение не выбрано, оно автоматически совершает транзакцию. (Конструктор класса Transaction действует как Функция более высокого порядка)

Мы также переместили функции расширения для Entity в пределах Transaction, чтобы эти функции расширения не были видны и не вызывались, не будучи в контексте этого класса. Сюда входят методы commit() и rollback(), которые могут быть вызваны только изнутри самого класса, поскольку теперь они являются функциями расширения, охватываемыми внутри класса.

Поскольку принимаемая лямбда является функцией расширения до Transaction, она работает в контексте этого класса и поэтому видит расширения. (см.: Литералы функций с ресивером)

Этот старый код теперь недействителен, когда компилятор сообщает нам об ошибке:

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

Пропускаемая лямбда выведена как функция расширения на Transaction, поскольку она не имеет формального объявления.

Чтобы связать кучу этих "действий" вместе внутри транзакции, просто создайте ряд функций расширения, которые могут использоваться в транзакции, например:

fun Transaction.actsInMovie(actor: Person, film: Movie) {
    film.addActor(actor)
    save(film)
}

Создайте больше, как это, а затем используйте их в лямбда, переданных в транзакцию...

Transaction { 
   actsInMovie(harrison, starWars)
   actsInMovie(carrie, starWars)
   directsMovie(abrams, starWars)
   rateMovie(starWars, 5)
}

Теперь, возвращаясь к исходному вопросу, у нас есть методы транзакций, и методы сущности появляются только в правильные моменты времени. И как побочный эффект использования lambdas или анонимных функций заключается в том, что мы в конечном итоге исследуем новые идеи о том, как скомпилирован наш код.

Ответ 2

См. другой ответ для основной темы и основ, здесь будут более глубокие воды...

Связанные расширенные темы:

Мы не решаем все, на что вы можете столкнуться здесь. Легко сделать некоторую функцию расширения в контексте другого класса. Но сделать эту работу не так просто, чтобы одновременно выполнить две вещи. Например, если бы я хотел, чтобы метод 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() доступны только в контексте.