Abstracción en programación orientada a objetos: qué es y ejemplos
La abstracción en programación orientada a objetos es el principio que nos permite representar únicamente las características y comportamientos importantes de un objeto, dejando fuera los detalles que no necesitamos conocer para utilizarlo.
Dicho de otra forma, la abstracción nos permite concentrarnos en qué hace un objeto en lugar de preocuparnos constantemente por todos los detalles de cómo lo hace. Es uno de los conceptos fundamentales de la programación orientada a objetos (POO) y resulta especialmente útil cuando desarrollamos sistemas grandes.
Un ejemplo cotidiano puede ayudarnos a entenderlo. Para conducir un automóvil utilizamos el volante, los pedales y la palanca de cambios, pero no necesitamos conocer cada proceso mecánico que ocurre dentro del motor.
En programación podemos aplicar una idea similar: ofrecer una interfaz sencilla mientras ocultamos o dejamos en segundo plano la complejidad necesaria para realizar una operación.
¿Qué es la abstracción en programación orientada a objetos?
La abstracción en POO consiste en identificar las características esenciales de una entidad y representarlas mediante una estructura que permita trabajar con ella sin depender de todos sus detalles internos.
Imagina que estamos desarrollando un sistema para administrar diferentes tipos de vehículos.
Podríamos identificar características comunes como:
- marca;
- modelo;
- velocidad;
- capacidad de arrancar;
- capacidad de detenerse.
Un automóvil y una motocicleta implementan internamente estas acciones de maneras que pueden ser diferentes.
Sin embargo, desde el punto de vista de nuestro programa podemos establecer una abstracción común:
Vehiculo
├── arrancar()
├── acelerar()
└── detener()
La clase Vehiculo representa el concepto general.
Después podemos crear implementaciones concretas:
Vehiculo
├── Automovil
├── Motocicleta
└── Camion
Nuestro código puede trabajar con la idea de un vehículo sin necesitar conocer desde el principio cada detalle de todos los vehículos posibles.
Ese es el poder de la abstracción.
Definición sencilla de abstracción en POO
Si necesitas una definición corta, puedes recordar esta:
La abstracción en programación orientada a objetos consiste en representar las características esenciales de un objeto y ofrecer una forma sencilla de utilizarlo, evitando que quien lo usa tenga que conocer todos sus detalles de implementación.
La palabra importante aquí es esencial.
Abstraer no significa eliminar información al azar.
Significa decidir qué información necesita conocer una parte del sistema y qué detalles pueden permanecer detrás de una interfaz más sencilla.
Un ejemplo de abstracción de la vida real
Pensemos en una cafetera.
Para preparar café normalmente realizamos algo parecido a esto:
- agregamos agua;
- colocamos café;
- presionamos un botón;
- esperamos.
Como usuarios no necesitamos controlar directamente la resistencia, la temperatura exacta del agua, la presión interna o cada componente eléctrico.
La cafetera nos proporciona una interfaz sencilla.
Podríamos representarla así:
class Cafetera:
def preparar_cafe(self):
print("Preparando café...")
El usuario solamente necesita hacer:
cafetera = Cafetera()
cafetera.preparar_cafe()
Pero internamente la operación podría ser mucho más compleja:
class Cafetera:
def preparar_cafe(self):
self._calentar_agua()
self._bombear_agua()
self._extraer_cafe()
self._servir()
def _calentar_agua(self):
print("Calentando agua")
def _bombear_agua(self):
print("Bombeando agua")
def _extraer_cafe(self):
print("Extrayendo café")
def _servir(self):
print("Sirviendo café")
Para preparar café seguimos utilizando:
cafetera.preparar_cafe()
La complejidad interna aumentó, pero la forma de utilizar el objeto no cambió.
Ese ejemplo muestra una de las ideas centrales de la abstracción: podemos interactuar con una funcionalidad mediante una interfaz simple sin necesitar controlar cada paso interno.
¿Para qué sirve la abstracción?
La abstracción ayuda principalmente a manejar la complejidad.
En una aplicación pequeña puede parecer innecesaria.
Pero imagina un proyecto formado por cientos de clases, servicios externos, bases de datos y miles de funciones.
Si cada parte del programa tuviera que conocer exactamente cómo funcionan todas las demás, realizar cualquier cambio sería complicado.
La abstracción permite establecer límites.
Una parte del sistema puede decir:
“Necesito guardar este usuario”.
Sin preocuparse necesariamente por si los datos se almacenarán en PostgreSQL, MySQL, un archivo o un servicio remoto.
Esa decisión puede quedar detrás de una abstracción.
¿Por qué es importante la abstracción en POO?
La abstracción en programación orientada a objetos aporta varios beneficios importantes.
Reduce la complejidad
Permite trabajar con conceptos de alto nivel sin pensar constantemente en todos sus detalles internos.
Facilita el mantenimiento
Una implementación puede cambiar sin obligar necesariamente a modificar el código que utiliza su interfaz.
Mejora la organización
Las responsabilidades pueden separarse mediante clases y contratos claramente definidos.
Favorece la reutilización
Diferentes clases pueden compartir una abstracción común.
Facilita las pruebas
Podemos sustituir determinadas implementaciones por versiones controladas durante las pruebas.
Reduce el acoplamiento
El código puede depender de una abstracción en lugar de estar conectado directamente a una implementación específica.
Este último punto es especialmente importante cuando comenzamos a desarrollar aplicaciones grandes.
¿Cómo funciona la abstracción en programación?
Podemos pensar en la abstracción como una separación entre dos preguntas:
¿Qué necesito hacer?
y:
¿Cómo se hace internamente?
Supongamos que tenemos un sistema de pagos.
Nuestra aplicación necesita realizar:
procesar_pago()
Pero existen diferentes proveedores:
PayPal
Tarjeta
Transferencia
Cada proveedor puede tener reglas y procesos diferentes.
Sin abstracción podríamos llenar nuestro programa de condiciones:
if metodo == "paypal":
# lógica de PayPal
elif metodo == "tarjeta":
# lógica de tarjeta
elif metodo == "transferencia":
# lógica de transferencia
Esto puede funcionar inicialmente, pero comienza a complicarse cuando aparecen nuevos métodos.
Podemos definir una abstracción:
MetodoPago
procesar_pago()
Y después crear diferentes implementaciones:
MetodoPago
├── PagoPayPal
├── PagoTarjeta
└── PagoTransferencia
Ahora todas las implementaciones comparten una idea común: procesar un pago.
Abstracción en Python
Python proporciona herramientas específicas para trabajar con clases abstractas mediante el módulo abc.
Las dos piezas que veremos con mayor frecuencia son:
ABC
y:
abstractmethod
Podemos importarlas así:
from abc import ABC, abstractmethod
Después podemos crear una clase abstracta.
from abc import ABC, abstractmethod
class Vehiculo(ABC):
@abstractmethod
def arrancar(self):
pass
Aquí Vehiculo representa una abstracción.
Estamos indicando que cualquier clase concreta que cumpla este contrato deberá proporcionar una implementación para:
arrancar()
¿Qué es una clase abstracta?
Una clase abstracta es una clase diseñada principalmente para definir una estructura o comportamiento común que otras clases deberán completar.
Normalmente no queremos crear objetos directamente de ella.
Por ejemplo:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def hacer_sonido(self):
pass
Animal representa una idea general.
Sabemos que los animales de nuestro modelo deben poder realizar una acción llamada:
hacer_sonido()
Pero la clase general no decide cuál será ese sonido.
Esa responsabilidad corresponde a las clases concretas.
¿Qué es un método abstracto?
Un método abstracto declara una operación que las clases derivadas deben implementar.
En Python podemos definirlo utilizando:
@abstractmethod
Por ejemplo:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def hacer_sonido(self):
pass
Después creamos:
class Perro(Animal):
def hacer_sonido(self):
return "Guau"
Y otra implementación:
class Gato(Animal):
def hacer_sonido(self):
return "Miau"
Ahora podemos utilizarlas:
perro = Perro()
gato = Gato()
print(perro.hacer_sonido())
print(gato.hacer_sonido())
Resultado:
Guau
Miau
Ambas clases respetan la misma abstracción, pero implementan el comportamiento de manera diferente.
Ejemplo completo de abstracción en Python
Veamos un ejemplo un poco más realista.
Queremos desarrollar un sistema que calcule el área de diferentes figuras geométricas.
Podemos crear una clase abstracta:
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def calcular_area(self):
pass
Ahora implementamos un círculo:
import math
class Circulo(Figura):
def __init__(self, radio):
self.radio = radio
def calcular_area(self):
return math.pi * self.radio ** 2
Después un rectángulo:
class Rectangulo(Figura):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def calcular_area(self):
return self.ancho * self.alto
Podemos utilizar ambas clases:
figuras = [
Circulo(5),
Rectangulo(4, 6)
]
for figura in figuras:
print(figura.calcular_area())
Nuestro bucle no necesita preguntar:
if isinstance(figura, Circulo):
ni:
if isinstance(figura, Rectangulo):
Simplemente sabe que cada objeto ofrece:
calcular_area()
La abstracción nos proporciona un contrato común.
Además, este ejemplo se combina naturalmente con otro concepto de POO: el polimorfismo.
¿Qué ocurre si no implementamos un método abstracto?
Esta es una ventaja práctica de utilizar ABC.
Imaginemos que creamos:
class Triangulo(Figura):
pass
Y después intentamos:
triangulo = Triangulo()
Python generará un error porque Triangulo no implementó el método abstracto:
calcular_area()
La clase base está obligando a sus implementaciones concretas a respetar un contrato mínimo.
Podemos solucionarlo así:
class Triangulo(Figura):
def __init__(self, base, altura):
self.base = base
self.altura = altura
def calcular_area(self):
return (self.base * self.altura) / 2
Ahora sí podemos crear:
triangulo = Triangulo(10, 5)
print(triangulo.calcular_area())
¿Se puede instanciar una clase abstracta en Python?
Si una clase contiene métodos abstractos pendientes, no podemos crear una instancia directamente.
Con esta clase:
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def calcular_area(self):
pass
esto no funcionará:
figura = Figura()
¿Y por qué querríamos evitarlo?
Porque Figura solamente representa el concepto general.
No tenemos información suficiente para calcular el área de una figura indeterminada.
Necesitamos algo concreto:
Circulo(5)
o:
Rectangulo(10, 4)
La clase abstracta define qué esperamos.
Las clases concretas definen cómo se realiza.
Una clase abstracta también puede tener métodos normales
Un error frecuente consiste en pensar que una clase abstracta solamente puede contener métodos abstractos.
No es así.
Podemos combinar ambos:
from abc import ABC, abstractmethod
class Empleado(ABC):
def __init__(self, nombre):
self.nombre = nombre
def mostrar_nombre(self):
return self.nombre
@abstractmethod
def calcular_salario(self):
pass
mostrar_nombre() ya tiene una implementación común.
Pero:
calcular_salario()
dependerá del tipo de empleado.
class EmpleadoFijo(Empleado):
def __init__(self, nombre, salario):
super().__init__(nombre)
self.salario = salario
def calcular_salario(self):
return self.salario
Podríamos crear otro tipo:
class EmpleadoPorHora(Empleado):
def __init__(self, nombre, horas, tarifa):
super().__init__(nombre)
self.horas = horas
self.tarifa = tarifa
def calcular_salario(self):
return self.horas * self.tarifa
Las clases comparten determinadas características y al mismo tiempo proporcionan su propia implementación para el comportamiento que varía.
Ejemplo práctico: sistema de pagos
Ahora utilicemos la abstracción en un escenario parecido a los que encontramos en aplicaciones reales.
Queremos aceptar diferentes métodos de pago.
Primero definimos la abstracción:
from abc import ABC, abstractmethod
class MetodoPago(ABC):
@abstractmethod
def pagar(self, cantidad):
pass
Implementamos tarjeta:
class PagoTarjeta(MetodoPago):
def pagar(self, cantidad):
print(f"Pagando ${cantidad} con tarjeta")
Después PayPal:
class PagoPayPal(MetodoPago):
def pagar(self, cantidad):
print(f"Pagando ${cantidad} con PayPal")
Y transferencia:
class PagoTransferencia(MetodoPago):
def pagar(self, cantidad):
print(f"Pagando ${cantidad} mediante transferencia")
Podemos crear una función:
def procesar_compra(metodo_pago, total):
metodo_pago.pagar(total)
Y utilizarla con cualquiera de nuestras implementaciones:
procesar_compra(PagoTarjeta(), 100)
procesar_compra(PagoPayPal(), 50)
procesar_compra(PagoTransferencia(), 75)
La función:
procesar_compra()
no necesita conocer los detalles específicos de cada proveedor.
Solamente espera recibir algo capaz de:
pagar()
Esto reduce la dependencia entre partes del sistema.
Abstracción y encapsulamiento: ¿son lo mismo?
No.
Esta es probablemente una de las confusiones más frecuentes cuando se estudia programación orientada a objetos.
La abstracción y el encapsulamiento están relacionados, pero solucionan problemas diferentes.
Podemos resumirlos así:
| Concepto | Pregunta principal |
|---|---|
| Abstracción | ¿Qué necesita conocer el usuario del objeto? |
| Encapsulamiento | ¿Cómo protegemos y controlamos el acceso a su estado interno? |
La abstracción busca representar solamente los aspectos relevantes y proporcionar una interfaz comprensible.
El encapsulamiento agrupa estado y comportamiento y controla cómo se accede a determinados detalles internos.
Veamos un ejemplo.
Ejemplo de abstracción y encapsulamiento
Supongamos que tenemos una cuenta bancaria:
class CuentaBancaria:
def __init__(self, saldo):
self._saldo = saldo
def depositar(self, cantidad):
if cantidad > 0:
self._saldo += cantidad
def obtener_saldo(self):
return self._saldo
El usuario interactúa mediante:
cuenta.depositar(100)
En lugar de necesitar conocer todos los pasos que podría realizar internamente el sistema bancario.
Ahí existe una abstracción de la operación.
Al mismo tiempo, _saldo representa un detalle interno que la clase intenta mantener controlado mediante sus métodos.
Eso se relaciona con encapsulamiento.
Una forma sencilla de recordarlo es:
La abstracción reduce lo que necesitamos comprender; el encapsulamiento controla cómo accedemos a los detalles internos.
En proyectos reales ambos principios suelen trabajar juntos.
Diferencia entre abstracción y ocultamiento de información
También es habitual utilizar “abstracción” y “ocultar información” como si fueran exactamente lo mismo.
Existe relación, pero no son conceptos idénticos.
Cuando abstraemos decidimos qué representación necesita el consumidor.
Cuando ocultamos información impedimos o desalentamos que otras partes dependan de determinados detalles internos.
Una API puede ser una abstracción aunque internamente existan numerosos componentes.
El usuario necesita conocer sus operaciones disponibles, parámetros y resultados.
No necesita conocer cada consulta SQL, estructura interna o servidor involucrado.
Abstracción y herencia
La herencia puede utilizarse para implementar determinadas abstracciones mediante jerarquías de clases.
En nuestro ejemplo:
Figura
├── Circulo
├── Rectangulo
└── Triangulo
Figura define una idea general.
Las clases concretas heredan de ella y completan el comportamiento.
Sin embargo, es importante no confundir ambos conceptos.
La abstracción responde a cómo representamos un concepto omitiendo detalles innecesarios.
La herencia es un mecanismo de relación entre clases.
Podemos utilizar herencia para construir abstracciones, pero abstracción y herencia no significan lo mismo.
Abstracción y polimorfismo
La abstracción también suele aparecer junto al polimorfismo.
Volvamos a nuestras figuras:
figuras = [
Circulo(3),
Rectangulo(4, 5),
Triangulo(10, 6)
]
for figura in figuras:
print(figura.calcular_area())
Todos los objetos responden a:
calcular_area()
La abstracción define la operación común.
El polimorfismo permite que la misma llamada produzca un comportamiento adecuado dependiendo del objeto concreto.
Podemos decir que estos conceptos se complementan.
Los cuatro pilares de la programación orientada a objetos
Tradicionalmente se explican cuatro conceptos fundamentales de POO:
- Abstracción
- Encapsulamiento
- Herencia
- Polimorfismo
Podemos compararlos rápidamente:
| Pilar | Idea principal | Ejemplo |
|---|---|---|
| Abstracción | Mostrar lo esencial | pagar() |
| Encapsulamiento | Controlar detalles internos | _saldo |
| Herencia | Compartir estructura y comportamiento | Perro(Animal) |
| Polimorfismo | Misma interfaz, distintos comportamientos | hacer_sonido() |
No funcionan como cuatro técnicas completamente aisladas.
En un buen diseño orientado a objetos suelen aparecer combinadas.
Abstracción mediante interfaces
En lenguajes como Java o C# encontramos el concepto explícito de interface.
Una interfaz define operaciones que una implementación debe proporcionar.
Conceptualmente podríamos tener:
RepositorioUsuario
guardar(usuario)
buscar(id)
eliminar(id)
Después:
RepositorioUsuario
├── RepositorioPostgreSQL
└── RepositorioMemoria
El resto de nuestra aplicación puede trabajar con:
RepositorioUsuario
sin preocuparse excesivamente por dónde están almacenados los datos.
Python tiene un modelo más dinámico, pero las clases abstractas mediante ABC permiten expresar contratos similares en muchos escenarios.
Abstracción mediante protocolos y duck typing en Python
Python también permite trabajar con abstracciones de una forma menos rígida.
Existe una frase conocida:
Si camina como pato y hace cuac como pato, probablemente podemos tratarlo como pato.
Es la idea detrás del duck typing.
Consideremos:
class Perro:
def hacer_sonido(self):
return "Guau"
class Gato:
def hacer_sonido(self):
return "Miau"
Podemos escribir:
def reproducir_sonido(animal):
print(animal.hacer_sonido())
Y utilizar:
reproducir_sonido(Perro())
reproducir_sonido(Gato())
Las clases no necesitan heredar de una clase Animal.
La función simplemente espera que el objeto proporcione:
hacer_sonido()
Esta flexibilidad forma parte del estilo de Python.
En código moderno con tipado estático opcional también podemos expresar contratos estructurales mediante Protocol del módulo typing.
Por ejemplo:
from typing import Protocol
class PuedeHablar(Protocol):
def hablar(self) -> str:
...
Una clase compatible podría ser:
class Persona:
def hablar(self) -> str:
return "Hola"
No necesita heredar explícitamente de PuedeHablar para satisfacer estructuralmente ese protocolo en herramientas de análisis de tipos.
Clase abstracta vs interfaz
Aunque el significado exacto depende del lenguaje, podemos establecer una comparación conceptual.
| Clase abstracta | Interfaz |
|---|---|
| Puede proporcionar comportamiento común | Define principalmente un contrato |
| Puede contener estado | Normalmente se centra en capacidades |
| Puede tener métodos implementados | Define operaciones esperadas |
| Se utiliza como base de una jerarquía | Se utiliza para desacoplar implementaciones |
En Python la frontera es menos rígida que en lenguajes como Java.
Podemos utilizar:
ABC;@abstractmethod;Protocol;- duck typing.
La elección depende del tipo de contrato que queramos expresar.
Ejemplo de abstracción con una base de datos
Veamos por qué la abstracción resulta especialmente útil en arquitectura de software.
Supongamos que nuestro servicio registra usuarios directamente en PostgreSQL:
class ServicioUsuario:
def registrar(self, usuario):
# conexión PostgreSQL
# consulta SQL
# guardar usuario
pass
Nuestro servicio ahora conoce demasiados detalles sobre almacenamiento.
Podemos crear una abstracción:
from abc import ABC, abstractmethod
class RepositorioUsuario(ABC):
@abstractmethod
def guardar(self, usuario):
pass
Implementación concreta:
class RepositorioPostgreSQL(RepositorioUsuario):
def guardar(self, usuario):
print("Guardando usuario en PostgreSQL")
Y nuestro servicio:
class ServicioUsuario:
def __init__(self, repositorio):
self.repositorio = repositorio
def registrar(self, usuario):
self.repositorio.guardar(usuario)
Ahora podemos hacer:
repositorio = RepositorioPostgreSQL()
servicio = ServicioUsuario(repositorio)
servicio.registrar("Leonel")
Si después queremos utilizar otra implementación:
class RepositorioMemoria(RepositorioUsuario):
def guardar(self, usuario):
print("Guardando usuario temporalmente en memoria")
podemos sustituirla sin modificar la lógica principal:
servicio = ServicioUsuario(
RepositorioMemoria()
)
Aquí podemos ver cómo la abstracción ayuda a reducir el acoplamiento.
Abstracción y dependencia de implementaciones
Observa estas dos ideas.
Primera:
ServicioUsuario -> PostgreSQL
Segunda:
ServicioUsuario -> RepositorioUsuario
↑
RepositorioPostgreSQL
En el segundo diseño, el servicio conoce el concepto:
RepositorioUsuario
pero no necesita depender directamente de todos los detalles de PostgreSQL.
Este tipo de diseño aparece constantemente en aplicaciones profesionales.
No significa que debamos crear una abstracción para absolutamente todo.
Significa que podemos utilizarlas cuando existe una variación o complejidad que merece ser aislada.
Abstracción en una API
Otro ejemplo fácil de reconocer es una API.
Imagina que utilizas:
cliente.enviar_mensaje(
destinatario="[email protected]",
mensaje="Hola"
)
Detrás podrían ocurrir muchas cosas:
- validar parámetros;
- autenticar una solicitud;
- convertir datos a JSON;
- abrir una conexión;
- realizar una petición HTTP;
- procesar la respuesta;
- gestionar errores.
Pero el consumidor trabaja con una operación sencilla:
enviar_mensaje()
Una buena API proporciona una abstracción útil sobre una operación más compleja.
De hecho, utilizamos abstracciones constantemente aunque no estemos trabajando explícitamente con clases abstractas.
Ejemplos de abstracción que usamos todos los días al programar
Considera esta línea:
archivo = open("datos.txt")
¿Necesitas conocer cada llamada que realiza el sistema operativo para acceder físicamente al almacenamiento?
Normalmente no.
O esta:
lista.sort()
Para utilizarla no necesitamos implementar el algoritmo interno de ordenamiento.
También:
print("Hola")
oculta numerosos detalles necesarios para mostrar información.
Las funciones y bibliotecas son formas de abstracción.
Por eso es importante entender que la abstracción es un concepto más amplio que simplemente utilizar ABC.
ABC es una herramienta concreta de Python para implementar determinados diseños abstractos.
Niveles de abstracción
Un sistema puede tener diferentes niveles de abstracción.
Por ejemplo, una aplicación de comercio electrónico podría trabajar así:
Interfaz de usuario
↓
Servicio de compras
↓
Servicio de pagos
↓
Proveedor de pagos
↓
Red
Cada capa puede proporcionar una abstracción a la capa superior.
La interfaz no necesita saber cómo se almacena una transacción.
El servicio de compras no necesita conocer todos los detalles de comunicación de red del proveedor.
Esta separación ayuda a manejar sistemas complejos.
Pero debemos encontrar un equilibrio.
Demasiadas capas innecesarias también pueden convertir un proyecto sencillo en algo difícil de entender.
Ventajas de utilizar abstracción
Veamos con mayor detalle sus principales beneficios.
Código más comprensible
Una buena abstracción permite pensar en conceptos relevantes para el problema.
Es más fácil leer:
pedido.calcular_total()
que encontrar repetida por todo el programa la lógica necesaria para calcular impuestos, descuentos y precios.
Menor acoplamiento
Las clases pueden depender de contratos en lugar de implementaciones específicas.
Esto facilita reemplazar componentes.
Mayor flexibilidad
Podemos agregar nuevas implementaciones sin modificar necesariamente el código que utiliza la abstracción.
Mejor mantenimiento
Los detalles internos pueden evolucionar mientras la interfaz se mantiene estable.
Pruebas más sencillas
Podemos reemplazar dependencias reales por implementaciones controladas.
Código reutilizable
Las operaciones comunes pueden ser utilizadas por diferentes clases.
Estos beneficios son especialmente visibles en sistemas que deben evolucionar durante años.
Desventajas de la abstracción
La abstracción tampoco es gratuita.
Aplicarla sin necesidad puede complicar un proyecto.
Puede aumentar la cantidad de código
En lugar de una clase podemos terminar con una interfaz, una clase abstracta y varias implementaciones.
Puede dificultar seguir el flujo
Demasiadas capas hacen que encontrar dónde ocurre realmente una operación sea más difícil.
Puede crear abstracciones incorrectas
Si agrupamos conceptos que realmente no comparten un comportamiento estable, la jerarquía terminará generando problemas.
Puede ser innecesaria
Si solamente existe una implementación sencilla y no existe una razón clara para aislarla, agregar varias capas podría no aportar valor.
Una buena regla práctica es:
Abstrae la complejidad que realmente necesitas gestionar, no la complejidad que imaginas que quizá exista algún día.
Errores comunes al aplicar abstracción
Crear clases abstractas para todo
No toda clase necesita una interfaz o clase base.
La abstracción debe solucionar un problema concreto.
Crear jerarquías demasiado profundas
Algo como:
Objeto
↓
Entidad
↓
SerVivo
↓
Animal
↓
Mamifero
↓
Canino
↓
Perro
puede ser correcto en determinados modelos, pero también puede convertirse rápidamente en una estructura difícil de mantener.
Confundir abstracción con ocultar variables
Utilizar:
_saldo
no convierte automáticamente una clase en una buena abstracción.
Exponer demasiados detalles
Una abstracción pierde valor si obliga al consumidor a comprender casi toda la implementación.
Diseñar contratos demasiado grandes
Si una abstracción exige demasiados métodos, algunas implementaciones pueden verse obligadas a proporcionar operaciones que realmente no necesitan.
En general, las abstracciones pequeñas y enfocadas suelen ser más fáciles de mantener.
¿Cuándo deberíamos utilizar una clase abstracta?
Una clase abstracta puede ser útil cuando varias clases representan diferentes versiones de un mismo concepto y queremos garantizar determinadas operaciones comunes.
Por ejemplo:
Notificacion
├── Email
├── SMS
└── Push
Podemos definir:
from abc import ABC, abstractmethod
class Notificacion(ABC):
@abstractmethod
def enviar(self, mensaje):
pass
Después cada implementación decide cómo enviar el mensaje.
Esta abstracción tiene sentido porque existe una capacidad común clara.
Ejemplo final: sistema de notificaciones
Construyamos el ejemplo completo:
from abc import ABC, abstractmethod
class Notificacion(ABC):
@abstractmethod
def enviar(self, destinatario, mensaje):
pass
class NotificacionEmail(Notificacion):
def enviar(self, destinatario, mensaje):
print(
f"Email enviado a {destinatario}: {mensaje}"
)
class NotificacionSMS(Notificacion):
def enviar(self, destinatario, mensaje):
print(
f"SMS enviado a {destinatario}: {mensaje}"
)
class NotificacionPush(Notificacion):
def enviar(self, destinatario, mensaje):
print(
f"Notificación push para {destinatario}: {mensaje}"
)
Ahora creamos una función que depende de la abstracción:
def avisar_usuario(
notificacion,
destinatario,
mensaje
):
notificacion.enviar(
destinatario,
mensaje
)
Podemos utilizar diferentes implementaciones:
email = NotificacionEmail()
sms = NotificacionSMS()
push = NotificacionPush()
avisar_usuario(
email,
"[email protected]",
"Tu pedido fue enviado"
)
avisar_usuario(
sms,
"+50370000000",
"Tu código es 1234"
)
avisar_usuario(
push,
"usuario-123",
"Tienes una nueva notificación"
)
La función avisar_usuario() no conoce los detalles internos de correo, SMS o notificaciones push.
Solamente conoce una operación abstracta:
enviar()
Si mañana agregamos WhatsApp:
class NotificacionWhatsApp(Notificacion):
def enviar(self, destinatario, mensaje):
print(
f"WhatsApp enviado a {destinatario}: {mensaje}"
)
el resto del diseño puede seguir funcionando con el mismo contrato.
¿Abstracción significa escribir menos código?
No necesariamente.
En algunos casos incluso escribiremos más código inicialmente.
La ventaja aparece en la estructura.
Supongamos que tenemos tres implementaciones.
Sin una abstracción clara, cada parte del sistema podría conocer directamente los detalles de las tres.
Con una buena abstracción, muchas partes solamente necesitan conocer el contrato común.
Por eso el objetivo no es reducir líneas.
El objetivo es reducir complejidad conceptual y dependencias innecesarias.
¿Es obligatorio usar abstracción en programación orientada a objetos?
No necesitamos crear clases abstractas en cada programa orientado a objetos.
Podemos desarrollar clases normales sin utilizar ABC.
Sin embargo, la idea de abstracción aparece constantemente cuando diseñamos software.
Cada vez que definimos una interfaz sencilla para una operación compleja estamos aplicando algún grado de abstracción.
Por eso resulta más útil aprender cómo pensar en abstracciones que memorizar únicamente la sintaxis de @abstractmethod.
Cómo identificar una buena abstracción
Una abstracción útil suele responder claramente a una responsabilidad.
Por ejemplo:
Repositorio
guardar()
buscar()
Notificador
enviar()
MetodoPago
pagar()
Los nombres expresan capacidades comprensibles.
Si necesitamos explicar durante diez minutos qué representa una interfaz de dos métodos, quizá el problema no sea quien la está leyendo.
Una buena abstracción debería permitir que el código exprese mejor la intención del programa.
Abstracción en proyectos reales
En proyectos profesionales podemos encontrar abstracciones alrededor de:
- bases de datos;
- almacenamiento de archivos;
- envío de correos;
- autenticación;
- pagos;
- notificaciones;
- servicios externos;
- sistemas de caché;
- colas de mensajes;
- generación de reportes.
Por ejemplo, nuestra aplicación podría depender de:
Almacenamiento
y tener:
AlmacenamientoLocal
AlmacenamientoS3
AlmacenamientoGoogleCloud
La aplicación necesita guardar archivos.
No debería necesitar conocer cada detalle de cada proveedor en todas sus capas.
Ese es exactamente el tipo de complejidad que una buena abstracción puede ayudarnos a controlar.
Abstracción vs encapsulamiento vs herencia vs polimorfismo
Antes de terminar, resumamos las diferencias.
Abstracción
Se concentra en qué características y operaciones son relevantes y deja en segundo plano detalles innecesarios.
Ejemplo:
pago.pagar()
Encapsulamiento
Controla el acceso al estado y detalles internos de un objeto.
Ejemplo:
cuenta._saldo
y métodos controlados para modificarlo.
Herencia
Permite establecer relaciones donde una clase deriva de otra.
Ejemplo:
class Perro(Animal):
pass
Polimorfismo
Permite utilizar una misma operación con diferentes implementaciones.
Ejemplo:
animal.hacer_sonido()
puede producir resultados diferentes dependiendo del objeto.
Los cuatro conceptos pueden colaborar dentro de un mismo diseño.
Preguntas frecuentes sobre abstracción en programación orientada a objetos
¿Qué es la abstracción en programación orientada a objetos?
La abstracción en POO consiste en representar las características esenciales de un objeto y proporcionar una interfaz para utilizarlo sin necesidad de conocer todos los detalles de su implementación.
¿Qué es un ejemplo de abstracción?
Una cafetera es un ejemplo cotidiano.
Presionamos un botón para preparar café sin controlar manualmente todos los procesos internos.
En programación, un método como:
cafetera.preparar_cafe()
puede ofrecer una interfaz sencilla para una operación internamente compleja.
¿Cómo se implementa la abstracción en Python?
Una opción consiste en utilizar clases abstractas mediante:
from abc import ABC, abstractmethod
Después podemos definir métodos que las clases derivadas deben implementar utilizando:
@abstractmethod
Python también permite trabajar con abstracciones mediante duck typing y Protocol.
¿Qué diferencia existe entre abstracción y encapsulamiento?
La abstracción se concentra en mostrar lo necesario para utilizar un componente sin depender de detalles irrelevantes.
El encapsulamiento controla cómo se organiza y accede al estado interno de un objeto.
Son conceptos relacionados, pero no equivalentes.
¿Qué es una clase abstracta?
Es una clase utilizada como base para definir una estructura o contrato común.
En Python una clase que hereda de ABC puede declarar métodos abstractos que las clases concretas deberán implementar.
¿Qué es ABC en Python?
ABC significa Abstract Base Class.
Forma parte del módulo abc de Python y puede utilizarse como base para definir clases abstractas.
¿Qué hace @abstractmethod?
Marca un método como abstracto.
Las clases concretas derivadas deberán proporcionar una implementación antes de poder ser instanciadas, mientras el método siga siendo abstracto en la jerarquía.
¿Una clase abstracta puede tener métodos normales?
Sí.
Una clase abstracta puede combinar métodos abstractos con métodos que ya contienen una implementación.
Esto permite compartir comportamiento común y obligar a las clases derivadas a implementar únicamente determinadas operaciones.
¿Abstracción y herencia son lo mismo?
No.
La abstracción representa únicamente los aspectos relevantes de un concepto.
La herencia es un mecanismo mediante el cual una clase puede derivar comportamiento y estructura de otra.
La herencia puede ayudarnos a implementar una abstracción, pero son conceptos diferentes.
¿Cuáles son los cuatro pilares de la POO?
Generalmente se presentan como:
- abstracción;
- encapsulamiento;
- herencia;
- polimorfismo.
Cada uno aborda un aspecto diferente del diseño orientado a objetos.
Conclusión
La abstracción en programación orientada a objetos nos permite manejar sistemas complejos concentrándonos en las características y operaciones realmente importantes para cada parte del programa.
En lugar de obligar a todo el código a conocer cada detalle de implementación, podemos definir contratos e interfaces que expresen claramente qué puede hacer un componente.
En Python podemos implementar este tipo de diseño mediante herramientas como ABC y @abstractmethod, aunque también existen enfoques más flexibles basados en duck typing y Protocol.
Lo más importante, sin embargo, no es memorizar una palabra clave.
Una buena abstracción aparece cuando conseguimos convertir algo complejo en una interfaz sencilla, coherente y útil sin ocultar información que el consumidor realmente necesita.
Cuando escribas una clase, prueba hacerte una pregunta: ¿qué necesita saber realmente quien utilice este objeto?
La respuesta suele ser un excelente punto de partida para diseñar una abstracción mejor.