Contenido del curso

Interfaces y polimorfismo en Kotlin

Resumen

Las interfaces en Kotlin funcionan como contratos que declaran qué métodos y variables debe cumplir una clase, sin definir cómo se implementan. Aprenderás a aplicarlas con el patrón repository y a entender por qué son la base del polimorfismo en programación orientada a objetos.

Qué es una interfaz y cómo se relaciona con el patrón repository

Una interfaz declara un conjunto de operaciones que una clase debe respetar. El patrón repository es un caso clásico: define métodos para crear, leer, actualizar y eliminar datos de la persistencia, sin importar cómo se realice esa persistencia internamente [0:09].

Piensa en un repositorio de correos. El contrato dice qué se puede hacer con un email, no cómo se guarda ni dónde. Esa separación entre el qué y el cómo es lo que llamamos abstracción.

¿Qué es una interfaz en Kotlin? Es un contrato que declara métodos y propiedades que una clase debe implementar, sin proporcionar la lógica concreta. Permite que distintas clases compartan un mismo comportamiento esperado.

Cómo se modela una clase Email con UUID

Dentro del ejercicio se crea una clase Email con tres propiedades: un id de tipo UUID, un subject como string y un body como string [0:38]. Usar UUID muestra que en Kotlin puedes apoyarte en clases de la librería estándar de Java, no solo en tipos primitivos.

Cómo se define el contrato EmailRepository

La interfaz EmailRepository declara cuatro operaciones esenciales [1:10]:

  • save(email) para guardar un correo.
  • findById(id: UUID): Email? que retorna un email o nulo, lo que introduce la nulabilidad como decisión explícita.
  • findAll(): List<Email> que devuelve todos los correos.
  • deleteEmailById(id: UUID) que elimina un correo y no retorna nada.

En ningún momento se especifica la lógica. Solo se firma el contrato.

Cómo implementar una interfaz con InMemoryEmailRepository

Para implementar una interfaz se usa la misma sintaxis que en la herencia: dos puntos seguidos del nombre del contrato. Si la clase no define todos los métodos, Kotlin marca el error Class is not abstract and does not implement abstract members [2:30].

El atajo Option + Enter o Control + Enter genera los stubs pendientes. La clase InMemoryEmailRepository simula persistencia con una mutableListOf<Email>() privada [3:15].

Por qué nombrar la implementación según su naturaleza

En lugar de llamarla EmailRepositoryImplementation, conviene InMemoryEmailRepository porque describe el mecanismo real. Si mañana añades una versión con base de datos, podrás crear DatabaseEmailRepository sin chocar con la primera. Múltiples clases pueden implementar el mismo contrato.

Cómo se implementan las cuatro operaciones del contrato

La lógica concreta usa funciones de la lista mutable [4:20]:

  • save: emails.add(email).
  • findById: emails.find { it.id == id } con una lambda function que filtra por id.
  • findAll: retorna la lista completa.
  • deleteEmailById: emails.removeIf { it.id == id }.

También puedes incluir propiedades en el contrato. Por ejemplo, val sizeEmails: Int, que en la implementación devuelve emails.size [5:30]. Quien usa la interfaz no necesita saber que detrás hay una lista; solo le importa cuántos correos existen.

Por qué las interfaces habilitan polimorfismo

Al declarar una variable con el tipo de la interfaz, puedes asignarle cualquier clase que cumpla el contrato. En el ejemplo, val emailsRepository: EmailRepository = InMemoryEmailRepository() [6:40].

Esto permite intercambiar implementaciones sin cambiar el resto del código. Si en el futuro necesitas una base de datos local, solo cambias la asignación.

¿Qué es el polimorfismo en programación orientada a objetos? Es la capacidad de que distintas clases implementen un mismo contrato de formas diferentes. Una variable tipada como interfaz puede recibir cualquier clase que cumpla esa interfaz.

Qué propiedades quedan visibles según el tipo declarado

Si tipas la variable como EmailRepository, solo verás los métodos del contrato. Una propiedad puntual como repositoryName, que existe únicamente en InMemoryEmailRepository, no será accesible salvo que tipes la variable explícitamente con la clase concreta [7:30]. Esa es la disciplina que impone la abstracción.

Cómo probar el flujo completo en main

El flujo de prueba incluye [8:15]:

  1. Crear un email con UUID.randomUUID(), asunto y mensaje.
  2. Imprimir el tamaño del repositorio antes y después del save para confirmar que pasa de 0 a 1.
  3. Listar todos los emails con findAll.
  4. Tomar el primer elemento, obtener su id y pasarlo a deleteById.
  5. Confirmar que el tamaño vuelve a 0.

Como el id se genera aleatoriamente en cada ejecución, no se puede hardcodear. Hay que obtenerlo del propio repositorio.

Ejercicio: EmailNotifier y la palabra clave override

El reto consiste en crear una interfaz EmailNotifier con un método notify(email) y una clase ConsoleNotifier que la implemente [10:30]. La gracia está en que mañana podrías cambiar ConsoleNotifier por AndroidNotifier que muestre la notificación en el centro de notificaciones del dispositivo, sin tocar el resto del código.

Dentro de ConsoleNotifier, el método aparece con la palabra override. Esa palabra es obligatoria cuando sobrescribes un método declarado en una interfaz o en una abstract class. Indica que estás dando la implementación concreta de un método que ya existía en el contrato [11:45].

¿Para qué sirve override en Kotlin? Sirve para señalar que estás reemplazando un método o propiedad heredado de una interfaz o clase abstracta. Sin override, el compilador no acepta la implementación.

La instanciación se hace forzando el tipo del contrato: val notifier: EmailNotifier = ConsoleNotifier(). Al llamar notifier.notify(email), se imprime el asunto del correo como notificación.

Esa es la belleza del diseño orientado a objetos: mientras la clase respete el contrato, el comportamiento esperado se cumple, sin importar la implementación interna. ¿Qué otra implementación de EmailNotifier se te ocurre para tu propio proyecto?