dsl builder kotlin

dsl - En Kotlin, ¿cómo agrego métodos de extensión a otra clase, pero solo visible en un contexto determinado?



builder (2)

En Kotlin, quiero agregar métodos de extensión a una clase, por ejemplo a la clase Entity . Pero solo quiero ver estas extensiones cuando la Entity está dentro de una transacción, de lo contrario está oculta. Por ejemplo, si defino estas clases y extensiones:

interface Entity {} fun Entity.save() {} fun Entity.delete() {} class Transaction { fun start() {} fun commit() {} fun rollback() {} }

Ahora puedo llamar accidentalmente a save() y delete() en cualquier momento, pero solo quiero que estén disponibles después del start() de una transacción y no después de commit() o rollback() . Actualmente puedo hacer esto, lo que está mal:

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

¿Cómo puedo hacer que aparezcan y desaparezcan en el contexto correcto?

Nota: esta pregunta está intencionalmente escrita y contestada por el autor ( Preguntas autocontenidas ), de modo que las respuestas idiomáticas a los temas de Kotlin más comunes estén presentes en SO. También para aclarar algunas respuestas realmente antiguas escritas para alfas de Kotlin que no son precisas para el día de Kotlin actual. Otras respuestas también son bienvenidas, ¡hay muchos estilos de cómo responder esto!


Los basicos:

En Kotlin, tendemos a usar las lambdas pasadas a otras clases para darles "alcance" o para tener un comportamiento que ocurra antes y después de que se ejecute la lambda, incluido el manejo de errores. Por lo tanto, primero debe cambiar el código de Transaction para proporcionar el alcance. Aquí hay una clase de Transaction modificada:

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

Aquí tenemos una transacción que, cuando se crea, requiere un lambda que realiza el procesamiento dentro de la transacción, si no se produce una excepción, la transacción se compromete automáticamente. (El constructor de la clase Transaction está actuando como una función de orden superior )

También hemos movido las funciones de extensión para que la Entity esté dentro de Transaction para que estas funciones de extensión no se vean ni se puedan llamar sin estar en el contexto de esta clase. Esto incluye los métodos de commit() y rollback() que solo se pueden llamar ahora desde la propia clase porque ahora son funciones de extensión dentro de la clase.

Dado que la lambda que se está recibiendo es una función de extensión de Transaction , opera en el contexto de esa clase y, por lo tanto, ve las extensiones. (ver: Funciones Literales con Receptor )

Este código antiguo ahora no es válido, y el compilador nos da un error:

fun changePerson(person: Person) { person.name = "Fred" person.save() // ERROR: unresolved reference: save() }

Y ahora escribirías el código en lugar de existir dentro de un bloque de Transaction :

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

La lambda que se está pasando se infiere como una función de extensión en Transaction ya que no tiene una declaración formal.

Para encadenar un montón de estas "acciones" juntas dentro de una transacción, simplemente cree una serie de funciones de extensión que se puedan usar dentro de una transacción, por ejemplo:

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

Crea más como esto, y luego úsalos en la lambda pasada a la Transacción ...

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

Ahora volviendo a la pregunta original, tenemos los métodos de transacción y los métodos de entidad que solo aparecen en los momentos correctos en el tiempo. Y como efecto secundario del uso de lambdas o funciones anónimas es que terminamos explorando nuevas ideas sobre cómo se compone nuestro código.


Ver la otra respuesta para el tema principal y los conceptos básicos, aquí hay aguas más profundas ...

Temas avanzados relacionados:

No resolvemos todo lo que pueda encontrar aquí. Es fácil hacer que una función de extensión aparezca en el contexto de otra clase. Pero no es tan fácil hacer que esto funcione para dos cosas al mismo tiempo. Por ejemplo, si quisiera que el método Movie addActor() apareciera solo dentro de un bloque de Transaction , es más difícil. El método addActor() no puede tener dos receptores al mismo tiempo. Entonces, o bien tenemos un método que recibe dos parámetros Transaction.addActorToMovie(actor, movie) o necesitamos otro plan.

Una forma de hacer esto es usar objetos intermedios mediante los cuales podemos extender el sistema. Ahora, el siguiente ejemplo puede o no ser sensato, pero muestra cómo pasar este nivel adicional de funciones de exposición solo como se desee. Aquí está el código, donde cambiamos Transaction para implementar una interfaz Transactable modo que ahora podemos delegar en la interfaz cuando lo deseemos.

Cuando agregamos una nueva funcionalidad, podemos crear nuevas implementaciones de Transactable que exponen estas funciones y también mantienen un estado temporal. Luego, una simple función auxiliar puede facilitar el acceso a estas nuevas clases ocultas. Todas las adiciones se pueden hacer sin modificar las clases originales básicas.

Clases básicas:

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

Más tarde, decidimos añadir:

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

Ahora usando la nueva funcionalidad:

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

O todo esto podría haber sido simplemente una función de extensión de nivel superior en Transactable si no le importara que estuviera en el nivel superior, no en una clase, y saturando el espacio de nombres del paquete.

Para otros ejemplos de uso de clases intermedias, vea:

  • en el módulo de configuración de Klutter TypeSafe, se utiliza un objeto intermediario para almacenar el estado de la "propiedad sobre la cual se puede actuar", por lo que se puede pasar y también cambiar los métodos disponibles. config.value("something").asString() ( enlace de código )
  • en el módulo Klutter Netflix Graph, se utiliza un objeto intermediario para hacer la transición a otra parte de la gramática DSL connect(node).edge(relation).to(otherNode) . ( enlace de código ) Los casos de prueba en el mismo módulo muestran más usos, incluida la forma en que incluso operadores como get() e invoke() están disponibles solo en contexto.