Cuando llevas un tiempo programando, empiezas a escuchar dos términos que aparecen constantemente en foros, libros y entrevistas: programación orientada a objetos y programación funcional. Son dos formas distintas de organizar y pensar el código, y entender la diferencia entre ellas no solo te hace mejor programador, sino que te ayuda a elegir el enfoque correcto para cada problema.
En esta guía aprenderás qué es cada paradigma, cuáles son sus principios fundamentales, cuándo conviene cada uno y cómo los lenguajes más populares los aplican en la práctica.
Qué es un paradigma de programación
Un paradigma de programación es una forma de pensar y organizar el código. No es una tecnología ni una librería: es una filosofía sobre cómo estructurar la solución a un problema. Los dos más importantes para un desarrollador moderno son la programación orientada a objetos (POO) y la programación funcional (PF).
La mayoría de lenguajes modernos no son puramente uno u otro: Python, JavaScript y Kotlin soportan ambos estilos y te dejan elegir cuál usar según el contexto. Java y C# son principalmente orientados a objetos pero han incorporado muchas características funcionales. Haskell y Clojure son casi puramente funcionales.
Programación orientada a objetos (POO)
La POO organiza el código alrededor de objetos: entidades que combinan datos (atributos) y comportamiento (métodos) en una sola unidad. La idea central es que el mundo real está lleno de objetos que tienen características y hacen cosas, y el código puede modelarse de la misma manera.
Una clase es el molde o plantilla para crear objetos. Un objeto es una instancia concreta de una clase.
# Python: clase y objetos
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular # atributo: dato
self._saldo = saldo_inicial # atributo privado (convención: _)
def depositar(self, cantidad): # método: comportamiento
if cantidad <= 0:
raise ValueError("La cantidad debe ser positiva")
self._saldo += cantidad
print(f"Depositados {cantidad}€. Saldo: {self._saldo}€")
def retirar(self, cantidad):
if cantidad > self._saldo:
raise ValueError("Saldo insuficiente")
self._saldo -= cantidad
print(f"Retirados {cantidad}€. Saldo: {self._saldo}€")
def consultar_saldo(self):
return self._saldo
def __repr__(self):
return f"CuentaBancaria(titular={self.titular!r}, saldo={self._saldo})"
# Crear objetos (instancias de la clase)
cuenta_ana = CuentaBancaria("Ana García", 1000)
cuenta_carlos = CuentaBancaria("Carlos López", 500)
cuenta_ana.depositar(200) # Depositados 200€. Saldo: 1200€
cuenta_ana.retirar(300) # Retirados 300€. Saldo: 900€
print(cuenta_ana.consultar_saldo()) # 900
# Cada objeto tiene su propio estado independiente
cuenta_carlos.depositar(100) # no afecta a cuenta_ana
Los cuatro pilares de la POO
Encapsulación
Agrupar los datos y el comportamiento relacionados en un objeto, y controlar qué partes del objeto son accesibles desde fuera. El objetivo es que el objeto gestione su propio estado interno y exponga solo lo necesario.
# Python: encapsulación
class Termostato:
def __init__(self, temperatura_inicial):
self._temperatura = temperatura_inicial # atributo "privado"
self._encendido = False
# Propiedad: acceso controlado al atributo privado
@property
def temperatura(self):
return self._temperatura
@temperatura.setter
def temperatura(self, nueva_temp):
if nueva_temp < 10 or nueva_temp > 35:
raise ValueError("Temperatura fuera del rango permitido (10-35°C)")
self._temperatura = nueva_temp
def encender(self):
self._encendido = True
def apagar(self):
self._encendido = False
t = Termostato(20)
print(t.temperatura) # 20 (acceso a través de la propiedad)
t.temperatura = 22 # modificación controlada
# t.temperatura = 50 → ValueError: fuera del rango
# No se puede acceder directamente al atributo privado (por convención)
# t._temperatura = 50 → funciona en Python pero viola la encapsulación
Herencia
Una clase puede heredar atributos y métodos de otra clase. Permite reutilizar código y crear jerarquías de clases donde las más específicas extienden el comportamiento de las más generales.
# Python: herencia
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def describir(self):
return f"{self.nombre} ({self.edad} años)"
def hacer_sonido(self):
raise NotImplementedError("Las subclases deben implementar este método")
class Perro(Animal):
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad) # llamar al constructor del padre
self.raza = raza
def hacer_sonido(self): # sobreescribir el método del padre
return "¡Guau!"
def describir(self):
return f"{super().describir()} - Raza: {self.raza}"
class Gato(Animal):
def hacer_sonido(self):
return "¡Miau!"
def ronronear(self):
return "Purrrrr..."
perro = Perro("Rex", 3, "Labrador")
gato = Gato("Luna", 5)
print(perro.describir()) # Rex (3 años) - Raza: Labrador
print(perro.hacer_sonido()) # ¡Guau!
print(gato.hacer_sonido()) # ¡Miau!
print(gato.ronronear()) # Purrrrr...
# isinstance verifica la jerarquía
print(isinstance(perro, Perro)) # True
print(isinstance(perro, Animal)) # True (Perro es un Animal)
print(isinstance(gato, Perro)) # False
Polimorfismo
Diferentes clases pueden responder al mismo mensaje (llamada a un método) de formas distintas. Permite escribir código que funciona con cualquier objeto que tenga el método correcto, sin importar de qué clase sea exactamente.
# Python: polimorfismo
animales = [Perro("Rex", 3, "Labrador"), Gato("Luna", 5), Perro("Toby", 2, "Poodle")]
# El mismo código funciona con cualquier Animal
for animal in animales:
print(f"{animal.nombre}: {animal.hacer_sonido()}")
# Rex: ¡Guau!
# Luna: ¡Miau!
# Toby: ¡Guau!
# Duck typing en Python: si tiene el método, funciona (sin herencia obligatoria)
class Robot:
def __init__(self, nombre):
self.nombre = nombre
def hacer_sonido(self):
return "Beep boop"
robot = Robot("R2D2")
todos = animales + [robot]
for entidad in todos:
print(f"{entidad.nombre}: {entidad.hacer_sonido()}")
# Robot también funciona aunque no hereda de Animal
Abstracción
Ocultar la complejidad interna y exponer solo lo necesario. El usuario de un objeto no necesita saber cómo funciona por dentro, solo qué puede hacer con él.
# Python: clases abstractas para definir contratos
from abc import ABC, abstractmethod
class FormaPago(ABC):
@abstractmethod
def cobrar(self, importe: float) -> bool:
"""Intenta cobrar el importe. Devuelve True si tuvo éxito."""
pass
@abstractmethod
def reembolsar(self, importe: float) -> bool:
pass
class TarjetaCredito(FormaPago):
def __init__(self, numero, titular):
self._numero = numero
self._titular = titular
def cobrar(self, importe):
# lógica real de cobro con Stripe, etc.
print(f"Cobrados {importe}€ a la tarjeta ...{self._numero[-4:]}")
return True
def reembolsar(self, importe):
print(f"Reembolsados {importe}€")
return True
class PayPal(FormaPago):
def __init__(self, email):
self._email = email
def cobrar(self, importe):
print(f"Cobrados {importe}€ via PayPal ({self._email})")
return True
def reembolsar(self, importe):
print(f"Reembolsados {importe}€ via PayPal")
return True
# El código de negocio trabaja con la abstracción, no con la implementación
def procesar_compra(forma_pago: FormaPago, total: float):
if forma_pago.cobrar(total):
print("Compra completada")
else:
print("Pago fallido")
procesar_compra(TarjetaCredito("4111111111111111", "Ana"), 99.99)
procesar_compra(PayPal("ana@mail.com"), 49.50)
# Misma función, diferentes implementaciones
Programación funcional (PF)
La programación funcional organiza el código alrededor de funciones puras y la transformación de datos. En lugar de objetos con estado mutable, trabaja con valores inmutables que se transforman mediante funciones. El objetivo es que el código sea predecible, fácil de razonar y de testear.
Funciones puras
Una función pura tiene dos características: dado el mismo input, siempre devuelve el mismo output, y no tiene efectos secundarios (no modifica nada fuera de la función).
# Python: función pura vs impura
# ❌ Función impura: modifica estado externo (efecto secundario)
total = 0
def sumar_impuro(n):
global total
total += n # modifica una variable externa
return total
sumar_impuro(5) # 5
sumar_impuro(5) # 10 ← mismo input, resultado diferente
sumar_impuro(5) # 15 ← no predecible
# ✅ Función pura: solo depende de sus parámetros, no modifica nada externo
def sumar_puro(acumulado, n):
return acumulado + n # crea un nuevo valor, no modifica nada
print(sumar_puro(0, 5)) # 5 siempre
print(sumar_puro(0, 5)) # 5 siempre
print(sumar_puro(0, 5)) # 5 siempre ← predecible y testeable
# Más ejemplos de funciones puras
def calcular_precio_con_iva(precio, iva=0.21):
return round(precio * (1 + iva), 2)
def filtrar_activos(usuarios):
return [u for u in usuarios if u["activo"]] # no modifica la lista original
def capitalizar_nombre(nombre):
return nombre.strip().title()
# Las funciones puras son fáciles de testear:
assert calcular_precio_con_iva(100) == 121.0
assert calcular_precio_con_iva(100, 0.10) == 110.0
# No necesitan base de datos, archivos ni estado previo para probarse
Inmutabilidad
En programación funcional, los datos no se modifican: en lugar de cambiar un valor, se crea uno nuevo con el cambio aplicado. Esto elimina una gran fuente de bugs: el estado compartido que se modifica desde distintos lugares.
# Python: trabajar con datos inmutables
# ❌ Estilo imperativo: modificar la lista original
def agregar_descuento_imperativo(productos):
for producto in productos:
producto["precio_final"] = producto["precio"] * 0.9 # modifica el original
return productos
# ✅ Estilo funcional: crear una nueva lista sin tocar la original
def agregar_descuento_funcional(productos):
return [
{**producto, "precio_final": producto["precio"] * 0.9}
for producto in productos
]
# {**producto, "precio_final": ...} crea un nuevo diccionario con todos los campos
# del original más el nuevo campo. El original no se toca.
productos = [
{"nombre": "Teclado", "precio": 80},
{"nombre": "Ratón", "precio": 40},
]
productos_con_descuento = agregar_descuento_funcional(productos)
print(productos[0]) # {"nombre": "Teclado", "precio": 80} ← sin cambios
print(productos_con_descuento[0]) # {"nombre": "Teclado", "precio": 80, "precio_final": 72.0}
Funciones de orden superior: map, filter y reduce
Las funciones de orden superior son funciones que reciben otras funciones como argumentos o devuelven funciones. Son el corazón del estilo funcional.
# Python: map, filter y reduce
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map: aplicar una función a cada elemento
cuadrados = list(map(lambda x: x ** 2, numeros))
print(cuadrados) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# filter: quedarse con los elementos que cumplen una condición
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares) # [2, 4, 6, 8, 10]
# reduce: combinar todos los elementos en uno
from functools import reduce
total = reduce(lambda acum, x: acum + x, numeros)
print(total) # 55 (la suma de todos)
# En Python moderno, las comprensiones de listas son más idiomáticas:
cuadrados = [x ** 2 for x in numeros]
pares = [x for x in numeros if x % 2 == 0]
// JavaScript: map, filter y reduce son métodos de array
const numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const cuadrados = numeros.map(x => x ** 2);
console.log(cuadrados); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
const pares = numeros.filter(x => x % 2 === 0);
console.log(pares); // [2, 4, 6, 8, 10]
const total = numeros.reduce((acum, x) => acum + x, 0);
console.log(total); // 55
// Encadenar transformaciones: estilo funcional puro
const resultado = numeros
.filter(x => x % 2 === 0) // solo pares: [2, 4, 6, 8, 10]
.map(x => x ** 2) // elevar al cuadrado: [4, 16, 36, 64, 100]
.reduce((sum, x) => sum + x, 0); // sumar: 220
console.log(resultado); // 220
Funciones lambda y como valores
# Python: las funciones son ciudadanos de primera clase
# Pueden asignarse a variables, pasarse como argumentos y devolverse desde funciones
# Asignar a variable
saludar = lambda nombre: f"Hola, {nombre}"
print(saludar("Ana")) # "Hola, Ana"
# Pasar como argumento
def aplicar_dos_veces(funcion, valor):
return funcion(funcion(valor))
duplicar = lambda x: x * 2
print(aplicar_dos_veces(duplicar, 3)) # 12 (3 → 6 → 12)
# Devolver desde una función (closure)
def crear_multiplicador(factor):
return lambda x: x * factor # devuelve una función
por_tres = crear_multiplicador(3)
por_cinco = crear_multiplicador(5)
print(por_tres(4)) # 12
print(por_cinco(4)) # 20
# Ordenar con función de clave
usuarios = [
{"nombre": "Carlos", "edad": 25},
{"nombre": "Ana", "edad": 30},
{"nombre": "María", "edad": 22},
]
por_edad = sorted(usuarios, key=lambda u: u["edad"])
por_nombre = sorted(usuarios, key=lambda u: u["nombre"])
La diferencia fundamental: estado mutable vs transformación de datos
La diferencia más importante entre los dos paradigmas es cómo tratan el estado.
En la POO, los objetos tienen estado interno que cambia con el tiempo. Un objeto CuentaBancaria tiene un saldo que se modifica cuando haces depósitos y retiradas. El objeto "recuerda" lo que ha pasado. El programa avanza modificando el estado de los objetos.
En la PF, los datos son inmutables. En lugar de modificar una cuenta, creas una nueva cuenta con el saldo actualizado. El programa avanza transformando datos en nuevos datos.
# El mismo problema resuelto con los dos estilos:
# Calcular el total de una lista de pedidos con descuento aplicado
pedidos = [
{"producto": "Teclado", "precio": 80, "cantidad": 1},
{"producto": "Ratón", "precio": 40, "cantidad": 2},
{"producto": "Monitor", "precio": 300, "cantidad": 1},
]
DESCUENTO = 0.10
# ─── Estilo OOP ────────────────────────────────────────────────────────────────
class Pedido:
def __init__(self, producto, precio, cantidad):
self.producto = producto
self.precio = precio
self.cantidad = cantidad
def subtotal(self):
return self.precio * self.cantidad
def subtotal_con_descuento(self, descuento):
return self.subtotal() * (1 - descuento)
class Carrito:
def __init__(self):
self._pedidos = []
def agregar(self, pedido):
self._pedidos.append(pedido)
def total(self, descuento=0):
return sum(p.subtotal_con_descuento(descuento) for p in self._pedidos)
carrito = Carrito()
for p in pedidos:
carrito.agregar(Pedido(p["producto"], p["precio"], p["cantidad"]))
print(f"Total: {carrito.total(DESCUENTO):.2f}€") # 378.00€
# ─── Estilo funcional ──────────────────────────────────────────────────────────
def subtotal(pedido):
return pedido["precio"] * pedido["cantidad"]
def aplicar_descuento(importe, descuento):
return importe * (1 - descuento)
total = sum(
aplicar_descuento(subtotal(p), DESCUENTO)
for p in pedidos
)
print(f"Total: {total:.2f}€") # 378.00€
Ambas soluciones son correctas. La OOP es más natural cuando los datos tienen ciclo de vida propio (una cuenta que persiste y cambia). La FP es más natural cuando son transformaciones de datos en un pipeline (tomar datos, transformarlos, obtener resultado).
Cuándo usar cada paradigma
La POO es más adecuada cuando:
- Modelas entidades del mundo real con estado propio: usuarios, cuentas, productos, conexiones.
- El estado de un objeto cambia con el tiempo de forma significativa.
- Necesitas reutilizar código mediante herencia y polimorfismo.
- Construyes interfaces gráficas, sistemas de juegos o simulaciones donde los objetos interactúan entre sí.
La programación funcional es más adecuada cuando:
- Transformas datos: parsear, filtrar, agregar, convertir.
- Construyes pipelines de procesamiento de datos.
- Necesitas código fácil de testear y razonar (las funciones puras son triviales de testear).
- Trabajas con concurrencia: sin estado mutable compartido, los problemas de hilos desaparecen.
- Quieres evitar bugs causados por efectos secundarios inesperados.
En la práctica, los mejores programas mezclan ambos enfoques. Python y JavaScript te permiten usar el estilo que mejor encaje con cada parte del sistema.
Resumen
- La POO organiza el código en objetos que combinan datos y comportamiento. Sus pilares son encapsulación, herencia, polimorfismo y abstracción. Es natural para modelar entidades con estado propio y ciclo de vida.
- La programación funcional organiza el código en funciones puras y datos inmutables. Su fuerza está en la predictibilidad, la testeabilidad y la composición de transformaciones.
- Una función pura siempre devuelve el mismo resultado para el mismo input y no tiene efectos secundarios. Son triviales de testear y de razonar.
- La inmutabilidad elimina una gran fuente de bugs: el estado mutable compartido que se modifica desde distintos lugares del código.
map,filteryreduceson las herramientas centrales del estilo funcional para transformar colecciones de datos.- La mayoría de lenguajes modernos soportan ambos estilos. No tienes que elegir uno para siempre: usa POO donde el estado y las entidades importan, y PF donde la transformación de datos importa.
No hay comentarios todavía. Sé el primero en compartir tu opinión.