Delegación De Clases En Kotlin

En este tutorial verás cómo aplicar delegación de clases en Kotlin a través de la palabra reservada by, la cual te permite generar los métodos delegados automáticamente.

Patrón De Delegación

Recordemos que el patrón de delegación es un patrón de diseño, cuyo objetivo es alcanzar reusabilidad en tus aplicativos a través de la composición de objetos.

Su implementación consta de un objeto recibidor que confía a otro objeto, llamado delegado, las peticiones asociadas a un contexto.

Y como esperamos que el delegado pueda variar la forma en que trata la petición, lo tipificamos con una interfaz para flexibilizar el diseño.

Por ejemplo:

Crear la delegación de las acciones «soltar ítems», «moverse» y «atacar» de un enemigo, basado en el poder de nivel que este posee.

Solución:

En primera instancia declaramos la interfaz que soporte las acciones requeridas:

interface PowerLevel {
    fun dropItems()
    fun attack()
    fun die()
}

Luego creamos un nivel de poder llamado Normal que representa a enemigos normales.

class Normal : PowerLevel {
    override fun dropItems() {
        println("Caen ítems normales")
    }

    override fun attack() {
        println("Normal te ataca")
    }

    override fun die() {
        println("Normal eliminado")
    }
}

Luego, creamos la clase para los enemigos, le añadimos como propiedad una instancia del tipo PowerLevel y creamos métodos para procesar las peticiones, a las cuales les delegamos las invocaciones de los métodos del nivel de poder.

class Enemy(private val powerLevel: PowerLevel) {
    fun dropItems() {
        powerLevel.dropItems()
    }

    fun attack() {
        powerLevel.attack()
    }

    fun die() {
        powerLevel.die()
    }
}

Seguido, probamos la creación de un enemigo normal en la función main(), para evidenciar como se facultan las acciones al delegado:

fun main() {
    val powerLevel = Normal()
    val monster1 = Enemy(powerLevel)
    monster1.dropItems()
    monster1.attack()
    monster1.die()
}

Salida:

Caen ítems normales
Normal te ataca
Normal eliminado

Ahora, ¿qué nos puede ofrecer Kotlin con respecto a esta implementación?

La Cláusula by

Con el fin de ahorrarte la escritura de la implementación de los métodos con la redirección a sus delegados, Kotlin soporta nativamente la delegación a través de la palabra reservada by. Donde el compilador de Kotlin generará automáticamente este código.

Para usar esta característica, implementa la interfaz del delegado sobre la clase receptora y luego ubica a la cláusula by, seguido del nombre de la propiedad o parámetro de clase (by nombrePropiedad).

Ejemplo: Reescribir la implementación de delegación anterior con la cláusula by.

class Enemy(private val powerLevel: PowerLevel) : PowerLevel by powerLevel

fun main() {
    val powerLevel = Normal()
    val monster1 = Enemy(powerLevel)
    monster1.dropItems()
    monster1.attack()
    monster1.die()
}

Salida:

Caen ítems normales
Normal te ataca
Normal eliminado

La palabra reservada by le indica al compilador que guarde una instancia de delegado en la clase solicitante. Y que cuando se ejecute una llamada al método del supertipo, entonces lo reenvíe al delegado.

El compilador de Kotlin genera el código para la delegación pero requiere que implementes la interfaz del delegado. Esta relación evidencia, que a la final para conseguir la delegación se propicia una implementación del patrón decorator.

Por lo que podrías extender el diseño de enemigos, al marcar a Enemy como abstracta y crear diferentes subclases de enemigos.

Sobrescritura De Métodos

Si deseas cambiar el comportamiento de los métodos que se usan para la delegación, entonces usa override para sobrescribir las sentencias como lo requieras.

Ejemplo: Sobrescribir el método die() de Enemy.

class Enemy(private val powerLevel: PowerLevel) : PowerLevel by powerLevel{
    override fun die() {
        println("¡Kabooom!")
    }
}

Salida:

Caen ítems normales
Normal te ataca
¡Kabooom!

Sobrescritura de propiedades

Si has declarado propiedades en la interfaz que responde a las peticiones, también es posible sobrescribirlas. Pero en este caso, los delegados usarán sus propios valores para ejecutar los métodos y no los de la clase receptora.

Ejemplo: Agregar una propiedad de bono para el ataque a la interfaz LevelPower y sobrescribir su valor en Enemy.

interface PowerLevel {
    val bonusAttack:Int
    fun dropItems()
    fun attack()
    fun die()
}

class Normal : PowerLevel {
    override val bonusAttack = 10

    override fun dropItems() {
        println("Caen ítems normales")
    }

    override fun attack() {
        println("Normal te ataca (bono de +$bonusAttack)")
    }

    override fun die() {
        println("Normal eliminado")
    }
}

class Enemy(private val powerLevel: PowerLevel) : PowerLevel by powerLevel{
    override val bonusAttack = 15
}

En Normal se ha implementado el get() de la propiedad para que el valor sea 10. El método attack() usará este valor para la impresión. Y en Enemy lo sobrescribimos a 15.

El resultado sería:

Caen ítems normales
Normal te ataca (bono de +10)
Normal eliminado

Claramente el valor de Enemy no es usado en la delegación de attack() hacia Normal.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!