ProgramaciónDesarrollador Kotlin Middle/Senior

¿Cómo funciona la delegación de comportamientos a través de interfaces en Kotlin (delegación por interfaz)? ¿Cuándo deberías usarla y en qué se diferencia de la delegación de propiedades y la herencia clásica?

Supere entrevistas con el asistente de IA Hintsage

Respuesta.

En Kotlin, la delegación de comportamientos se implementa mediante la palabra clave by directamente en la firma de la clase. Esto permite enviar automáticamente las llamadas a los métodos de la interfaz (o de varias interfaces) a otro objeto con una implementación, reduciendo el boilerplate y facilitando la composición.

Historia del asunto

La aparición de la delegación de interfaces es un intento de eliminar las limitaciones y desventajas de la herencia múltiple. Es la idea de "composición sobre herencia": delegamos comportamientos sin recurrir a la jerarquía de clases. Tomado de lenguajes donde la composición es más popular (por ejemplo, Go, Scala).

Problema

En Java y otros lenguajes, a menudo es necesario crear una interfaz y realizar manualmente cada método, transfiriendo la lógica a otro campo (patrón Adapter de Objeto), lo cual rápidamente se vuelve obsoleto a medida que crece el número de métodos.

Solución

Kotlin permite delegar declarativamente una interfaz usando by:

interface Logger { fun log(msg: String) } class ConsoleLogger: Logger { override fun log(msg: String) = println(msg) } class Service(logger: Logger): Logger by logger { fun doWork() { log("Trabajo iniciado") // ... } } val service = Service(ConsoleLogger()) service.doWork()
  • Todos los métodos de Logger se implementan a través del objeto logger proporcionado, y no es necesario en la clase Service redefinir o proxear explícitamente los métodos.

Características clave:

  • Permite separar la implementación de la interfaz de su uso, reduciendo la duplicación de código
  • La delegación es más flexible que la herencia y funciona con múltiples comportamientos
  • Soporta las mejores prácticas SOLID

Preguntas engañosas.

¿Qué sucede si se agrega un método propio de la interfaz en la clase Service con la misma firma?

La implementación propia "anula" la delegada: es decir, prevalece el método definido explícitamente en la clase:

class Service(logger: Logger): Logger by logger { override fun log(msg: String) = println("PREFIX: $msg") }

¿Puede una clase delegar múltiples interfaces a diferentes objetos?

Sí, una clase puede implementar y delegar múltiples interfaces a diferentes objetos, pero cada interfaz se delega a un solo objeto:

class Service( logger: Logger, tracker: Tracker ): Logger by logger, Tracker by tracker

¿En qué se diferencia la delegación de interfaz de la delegación de propiedades mediante by?

  • La delegación de interfaz transfiere toda la implementación de las funciones de la interfaz a otro objeto.
  • La delegación de propiedades (property delegation) delega el trabajo de get/set a un objeto delegado de tipo específico (ReadOnlyProperty, ReadWriteProperty).

Errores comunes y anti-patrones

  • Delegar interfaces demasiado grandes (violación de ISP)
  • Aplicar simultáneamente implementación explícita y delegación (comportamiento inesperado)
  • Intentar combinar la delegación de interfaces y herencia con una clase base, ignorando el orden de resolución de métodos

Ejemplo de la vida real

Caso negativo

La clase implementa manualmente la interfaz, cada método llama al delegado, al agregar nuevos métodos se olvida actualizar la proxy, lo que lleva a errores.

Ventajas:

  • Se controla explícitamente la lógica

Desventajas:

  • Alto riesgo de errores, boilerplate
  • Escala mal a medida que crece la interfaz

Caso positivo

Se utiliza la delegación del lenguaje, solo los métodos no estándar se implementan dentro de la clase, nueva funcionalidad se añade sin grandes cambios.

Ventajas:

  • Mínimo código
  • Control claro sobre los puntos de extensión

Desventajas:

  • Requiere atención al implementar combinaciones (puedes oscurecer fácilmente el método delegado con tu propia implementación)