Las abstract classes y las interfaces son dos conceptos clave en la programación orientada a objetos que permiten estructurar código de manera eficiente. En este contenido profundizamos en entender qué son estas abstract classes, en qué se diferencian de las interfaces y en qué situaciones es recomendable utilizar cada una para lograr un código más limpio y escalable.
¿Qué es una abstract class y cómo funciona?
Una abstract class es una clase especial en programación que define métodos y variables para que otras clases, mediante la herencia, implementen sus funcionalidades. A diferencia de las clases comunes, estas no pueden instanciarse directamente, ya que contienen métodos abstractos y variables abstractas que deben ser obligatoriamente definidos por las subclases que heredan de ellas.
La declaración se realiza con la palabra reservada abstract.
Garantiza que cualquier clase que la extienda debe implementar sus métodos abstractos definidos en la clase base.
¿Cómo declarar una abstract class?
Para declarar una abstract class, sigue el modelo siguiente:
Cuando otra clase extiende esta clase abstracta, debe implementar el método.
¿En qué se diferencian abstract classes e interfaces?
La principal diferencia radica en cómo establecen relaciones entre las clases que las implementan:
Las abstract classes usan herencia, creando relaciones directas entre una clase padre y sus subclases. Esto implica una jerarquía y dependencia.
Las interfaces usan composición y no generan esta jerarquía. Permiten crear pequeñas funcionalidades encapsuladas y mantener clases más independientes.
Al decidir entre ellas, considera:
Usar abstract class si todas las variables o métodos se utilizarán frecuentemente en subclases.
Usar interfaces para pequeños contratos flexibles que no necesariamente requerirán todas sus variables o métodos en todas las implementaciones.
¿Qué implica el uso de herencia versus composición?
La herencia puede llevar a una acumulación de métodos y variables innecesarias en algunas clases hijas, mientras que la composición mediante interfaces permite un diseño modular y más liviano.
De este modo, interfaces ayudan a mantener un código ordenado y limpio, facilitando futuras modificaciones o ampliaciones.
Ejemplo práctico con abstract class
Aquí tienes un ejemplo práctico que ilustra cómo usar abstract class:
Recuerda, implementas la función notify obligatoriamente en la subclase cuando usas abstract class, algo similar a la implementación requerida por una interfaz, pero mediante herencia.
¿Cuándo elegir abstract class sobre interfaces?
Elige abstract class si:
Necesitas mantener y extender una lógica común de manera sencilla a través de múltiples implementaciones relacionadas por jerarquía.
Opta por interfaces cuando:
Prefieres flexibilidad y modularidad, evitando relaciones jerárquicas rígidas entre las clases que implementan funcionalidades similares.
<u>Diferencias entre </u>interface<u> y </u>abstract class
EmailRepository y NotifyEmailRepository<u>:</u>
👉 No almacena datos ni estado.
👉 Define un contrato puro, sin lógica.
👉 Cualquier clase (en memoria, SQLite, remota) puede implementarla de forma diferente.
class InMemoryEmailRepositoryImpl : EmailRepository
👉 Implementa la interface, proporcionando una implementación concreta en memoria.
👉 La lógica es 100% propia.
abstract class NotifyEmailRepositoryImpl(...) : NotifyEmailRepository
Es unaabstract classcon lógica común:
Guarda el emailRepository como dependencia.
Implementa notifyById(id) (método común).
Obliga a subclases a definir notify(email) → comportamiento específico.
💡 Esta clase es un template: resuelve lo repetido y fuerza a que cada implementación defina su variante.
NotifySentEmailRepositoryImpl y NotifyReceivedEmailRepositoryImpl
Son clases concretas, que hereda la lógica común de NotifyEmailRepositoryImpl
Solo definen qué imprimir cuando se notifica un envío.
👉 Siguen el patrón: reutilizar lógica de búsqueda y solo definir cómo se notifica.
import java.util.UUIDdata classEmails( val id:UUID, val asunto:String, val mensaje:String)// InterfaceinterfaceEmailRepository{ val sizeEmails:Int fun save(email:Emails) fun findById(id:UUID):Emails? fun findAll():List<Emails> fun deleteById(id:UUID)}classInMemoryEmailRepositoryImpl:EmailRepository{ val repositoryName:String="InMemoryEmailRepositoryImpl"private val emails = mutableListOf<Emails>() override val sizeEmails:Intget()= emails.size override fun save(email:Emails){ emails.add(email)} override fun findById(id:UUID):Emails?{return emails.find{ it.id== id }} override fun findAll():List<Emails>{return emails
} override fun deleteById(id:UUID){ emails.removeIf{ it.id== id }}}// abstractinterfaceNotifyEmailRepository{ fun notifyById(id:UUID)}abstract classNotifyEmailRepositoryImpl(private val emailRepository:EmailRepository):NotifyEmailRepository{/*
Se usa **final override** para evitar que subclases vuelvan a sobrescribir
ese método (notifyById). Así asegurás que la lógica de búsqueda no se modifique.
*/ final override fun notifyById(id:UUID){ emailRepository.findById(id)?.let {notify(it)}}/*
protected: solo las subclases pueden usarla.
abstract: obliga a cada subclase a implementar cómo notificar.
Así, cada subclase solo define el println, pero no toca la lógica de búsqueda.
*/protected abstract fun notify(email:Emails)}classNotifySentEmailRepositoryImpl(repository:EmailRepository):NotifyEmailRepositoryImpl(repository){ override fun notify(email:Emails){println("Email enviado: ${email.asunto}")}}classNotifyReceivedEmailRepositoryImpl(repository:EmailRepository):NotifyEmailRepositoryImpl(repository){ override fun notify(email:Emails){println("Tienes un nuevo email: ${email.asunto}")}}fun main(){ val emailRepository:EmailRepository=InMemoryEmailRepositoryImpl() val notifySentEmailRepository:NotifyEmailRepository=NotifySentEmailRepositoryImpl(emailRepository) val notifyReceivedEmailRepository:NotifyEmailRepository=NotifyReceivedEmailRepositoryImpl(emailRepository)println(emailRepository.sizeEmails) emailRepository.save(Emails( id =UUID.randomUUID(), asunto ="Hola", mensaje ="Que tal")) notifySentEmailRepository.notifyById(emailRepository.findAll()[0].id) notifyReceivedEmailRepository.notifyById(emailRepository.findAll()[0].id) emailRepository.save(Emails( id =UUID.randomUUID(), asunto ="Hola 2", mensaje ="Que tal 2")) notifySentEmailRepository.notifyById(emailRepository.findAll()[1].id) notifyReceivedEmailRepository.notifyById(emailRepository.findAll()[1].id)println(emailRepository.sizeEmails)println(emailRepository.findAll())println(emailRepository.findById(emailRepository.findAll()[0].id)) emailRepository.deleteById(emailRepository.findAll()[0].id)println(emailRepository.findAll())}
¿Qué pasa si instancio una clase abstracta?
El compilador de Kotlin te marcará un error inmediato y no te permitirá ejecutar el código. Las clases abstractas son, por definición, plantillas incompletas.
Imagina que una clase abstracta es un plano arquitectónico de una casa. No puedes vivir dentro de un plano de papel; necesitas construir la casa real primero. Al usar la palabra reservada abstract, le estás diciendo a Kotlin: "Esta clase tiene ideas y reglas, pero le faltan detalles concretos". Para poder usarla, obligatoriamente debes crear una nueva clase que herede de ella (usando : NombreClaseAbstracta()), completar los métodos faltantes marcados con override y, finalmente, instanciar esa nueva clase hija.
Mi aporte a la resolucion del ejercicio:
Esta vez cai en cuenta de mi error anterior (println("Nuevo email: ${email.asunto}")
Y si lo imprimi.
Me falto haber llamado al metodo notify con una variable como lo hace el profe con val notifier = ConsoleNotifier( )