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]:
- Crear un email con
UUID.randomUUID(), asunto y mensaje.
- Imprimir el tamaño del repositorio antes y después del
save para confirmar que pasa de 0 a 1.
- Listar todos los emails con
findAll.
- Tomar el primer elemento, obtener su
id y pasarlo a deleteById.
- 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?