Tema 45 – PROFUNDIZADO
Programación: Evolución, Paradigmas y Tendencias Modernas
Análisis exhaustivo de paradigmas, casos prácticos y arquitecturas avanzadas para TFA-STI SAS
Introducción – Una Visión Panorámica
La programación ha experimentado una transformación radical en los últimos 60 años. Desde los primeros lenguajes imperativos basados en procedimientos hasta las sofisticadas arquitecturas orientadas a eventos y aspectos de hoy, cada paradigma ha traído consigo soluciones específicas a problemas específicos. Este documento profundiza exhaustivamente en cada paradigma, explorando no solo sus conceptos teóricos sino también sus aplicaciones prácticas, ventajas, desventajas y casos de uso reales. Entender estos paradigmas es fundamental para cualquier profesional de TI que desee escribir software mantenible, escalable y de calidad en contextos complejos como el del Servicio Andaluz de Salud.
1. Evolución de la Programación: Un Recorrido Histórico Detallado
1.1 Era 1: Primeros Lenguajes (1950s-1960s) – Assembly, COBOL, FORTRAN
En los albores de la computación, los programadores escribían directamente en lenguaje máquina y ensamblador. El código era específico de la máquina, difícil de mantener y propenso a errores. No existía abstracción alguna.
Características:
- Código muy cercano al hardware
- Sin capacidades de abstracción
- Extremadamente tedioso y propenso a errores
- FORTRAN (1954) fue revolucionario al permitir escribir fórmulas matemáticas directamente
- COBOL (1959) permitía escribir en lenguaje más cercano al inglés
1.2 Era 2: Programación Estructurada (1970s-1980s)
Dijkstra publicó en 1968 su famoso artículo «Go To Statement Considered Harmful», provocando una revolución en cómo se escribía código. La introducción de estructuras de control (if/else, while, for) sin GOTO eliminó el «código spaghetti».
1.3 Era 3: Programación Orientada a Objetos (1980s-1990s)
La POO surgió como respuesta a la necesidad de mejor modularidad y reutilización. Smalltalk (1972-1980) fue un precursor, pero C++ (1983) y después Java (1995) popularizaron el paradigma.
Impacto revolucionario:
- Los datos y comportamiento se agrupan en objetos
- Encapsulación permite proteger el estado interno
- Herencia facilita reutilización masiva de código
- Polimorfismo permite escribir código genérico
- Patrones de diseño se hicieron posibles (GOF 1994)
1.4 Era 4: Web y Multiparadigma (2000s)
Con la explosión de Internet, lenguajes dinámicos como Python, Ruby y PHP ganaron popularidad. JavaScript trajo la programación a navegadores web. Se reconoció que un solo paradigma no era suficiente.
1.5 Era 5: Moderno (2010s-Actual)
Hoy coexisten múltiples paradigmas. Go, Rust, TypeScript, Kotlin son multiparadigma. Event-Driven Architecture, Microservicios, Serverless, y IA son paradigmas emergentes.
2. Programación Estructurada: El Fundamento
2.1 Concepto Detallado
La programación estructurada es un paradigma imperativo que organiza el código mediante procedimientos y funciones con control de flujo explícito mediante condicionales y bucles, eliminando saltos incondicionales (GOTO).
Los Tres Constructos Fundamentales:
// 1. SECUENCIA: Ejecución lineal de instrucciones
instruccion1();
instruccion2();
instruccion3();
// 2. DECISIÓN: Ejecución condicional
if (condicion) {
hacer_algo_A();
} else {
hacer_algo_B();
}
// 3. ITERACIÓN: Repetición de bloques
while (condicion) {
hacer_algo();
}
for (int i = 0; i < 10; i++) {
hacer_algo(i);
}
2.2 Ventajas y Limitaciones
Ventajas:
- Fácil de aprender y entender
- Excelente para algoritmos secuenciales
- Bajo overhead de ejecución
- Bueno para programas pequeños a medianos
Limitaciones:
- Difícil de mantener en proyectos grandes
- Poca reutilización de código
- Sin encapsulación de datos
- Variables globales generan acoplamiento
3. Programación Orientada a Objetos: Profundización Exhaustiva
3.1 Concepto General
La POO es un paradigma que estructura el software alrededor de objetos que contienen tanto datos (estado) como comportamiento (métodos). Los objetos interactúan enviándose mensajes entre ellos, simulando el mundo real.
3.2 Pilar 1: Encapsulación - Protección y Control
Definición técnica: Encapsulación es el mecanismo de agrupar datos y métodos relacionados en una clase, y controlar el acceso externo mediante modificadores de acceso (public, private, protected).
Ejemplo Práctico Detallado - Cuenta Bancaria:
// MALA PRÁCTICA: Sin encapsulación
class CuentaBancaria {
public double saldo = 1000.00; // ¡VULNERABLE!
}
// Cualquiera puede manipular el saldo
cuenta.saldo = -999999; // ¡Desastre!
// BUENA PRÁCTICA: Con encapsulación
class CuentaBancaria {
private double saldo; // Privado - no accesible directamente
private String numeroCuenta;
private List<String> historialTransacciones;
// Constructor
public CuentaBancaria(String numeroCuenta, double saldoInicial) {
this.numeroCuenta = numeroCuenta;
this.saldo = saldoInicial;
this.historialTransacciones = new ArrayList<>();
registrarTransaccion("Apertura de cuenta: " + saldoInicial);
}
// Getter controlado
public double obtenerSaldo() {
return saldo;
}
// Método de depósito con validación
public void depositar(double cantidad) {
if (cantidad <= 0) {
throw new IllegalArgumentException("Cantidad debe ser positiva");
}
saldo += cantidad;
registrarTransaccion("Depósito: +" + cantidad);
}
// Método de retiro con validación
public void retirar(double cantidad) {
if (cantidad <= 0) {
throw new IllegalArgumentException("Cantidad debe ser positiva");
}
if (cantidad > saldo) {
throw new IllegalArgumentException("Fondos insuficientes");
}
saldo -= cantidad;
registrarTransaccion("Retiro: -" + cantidad);
}
// Método privado (solo uso interno)
private void registrarTransaccion(String descripcion) {
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
historialTransacciones.add(timestamp + ": " + descripcion);
}
// Método para obtener historial
public List<String> obtenerHistorial() {
return new ArrayList<>(historialTransacciones); // Retorna copia
}
}
Beneficios Demostrados:
- Protección de datos: Imposible corromper el saldo directamente
- Validación: Cada operación se valida antes de ejecutarse
- Auditoría: Historial automático de todas las transacciones
- Mantenibilidad: Cambios internos no afectan código cliente
- Seguridad: Métodos privados no son accesibles externamente
3.3 Pilar 2: Herencia - Reutilización y Jerarquía
Definición técnica: La herencia permite que una clase hija herede atributos y métodos de una clase padre, estableciendo relaciones "es-un-a" y promoviendo reutilización.
Ejemplo Jerárquico - Sistema de Vehículos:
// Clase base (superclase)
abstract class Vehiculo {
protected String marca;
protected String modelo;
protected int año;
protected double velocidadMaxima;
public Vehiculo(String marca, String modelo, int año, double velocidadMaxima) {
this.marca = marca;
this.modelo = modelo;
this.año = año;
this.velocidadMaxima = velocidadMaxima;
}
// Método abstract (obliga a las subclases a implementarlo)
abstract void acelerar();
abstract void frenar();
// Método concreto (heredado por todas)
public void mostrarInfo() {
System.out.println("Vehículo: " + marca + " " + modelo + " (" + año + ")");
System.out.println("Velocidad máxima: " + velocidadMaxima + " km/h");
}
}
// Primera subclase - Automóvil
class Automovil extends Vehiculo {
private int numPuertas;
private boolean esConvertible;
public Automovil(String marca, String modelo, int año,
double velocidadMaxima, int numPuertas, boolean esConvertible) {
super(marca, modelo, año, velocidadMaxima); // Llamar constructor padre
this.numPuertas = numPuertas;
this.esConvertible = esConvertible;
}
@Override // Override del método abstract
void acelerar() {
System.out.println("El automóvil acelera suavemente");
}
@Override
void frenar() {
System.out.println("El automóvil frena con el sistema ABS");
}
// Métodos específicos de Automóvil
public void abrirTecho() {
if (esConvertible) {
System.out.println("Techo abierto");
}
}
}
// Segunda subclase - Motocicleta
class Motocicleta extends Vehiculo {
private boolean tieneSidecar;
public Motocicleta(String marca, String modelo, int año,
double velocidadMaxima, boolean tieneSidecar) {
super(marca, modelo, año, velocidadMaxima);
this.tieneSidecar = tieneSidecar;
}
@Override
void acelerar() {
System.out.println("La motocicleta acelera agresivamente");
}
@Override
void frenar() {
System.out.println("La motocicleta frena con frenado en cascada");
}
public void hacerCaballito() {
System.out.println("¡Haciendo caballito!");
}
}
// Tercera subclase - Camión
class Camion extends Vehiculo {
private double capacidadCarga; // en toneladas
public Camion(String marca, String modelo, int año,
double velocidadMaxima, double capacidadCarga) {
super(marca, modelo, año, velocidadMaxima);
this.capacidadCarga = capacidadCarga;
}
@Override
void acelerar() {
System.out.println("El camión acelera lentamente (carga pesada)");
}
@Override
void frenar() {
System.out.println("El camión frena con sistemas de frenado doble");
}
public void cargar(double peso) {
if (peso <= capacidadCarga) {
System.out.println("Cargando " + peso + " toneladas");
} else {
System.out.println("Sobrecarga: excede " + capacidadCarga + " toneladas");
}
}
}
// Uso del sistema
public class SistemaVehiculos {
public static void main(String[] args) {
// Crear diferentes tipos de vehículos
Vehiculo auto = new Automovil("Toyota", "Corolla", 2024, 180, 4, false);
Vehiculo moto = new Motocicleta("Harley", "Davidson", 2024, 220, false);
Vehiculo camion = new Camion("Volvo", "FH16", 2024, 120, 25.0);
// Polimorfismo en acción
Vehiculo[] flotas = {auto, moto, camion};
for (Vehiculo v : flotas) {
v.mostrarInfo();
v.acelerar();
v.frenar();
System.out.println("---");
}
}
}
Ventajas de esta Estructura:
- DRY (Don't Repeat Yourself): Código común en clase base
- Jerarquía clara: Relación "es-un-a" evidente
- Extensibilidad: Fácil agregar nuevos tipos de vehículos
- Mantenimiento centralizado: Cambios en base se propagan
3.4 Pilar 3: Abstracción - Ocultamiento de Complejidad
Definición técnica: La abstracción es el proceso de simplificar objetos complejos exponiendo solo las características esenciales necesarias, ocultando detalles de implementación.
Ejemplo - Interface de Pago Abstracta:
// Interfaz abstracta (contrato)
interface ProcesadorPago {
boolean procesarPago(double cantidad);
boolean reembolsar(double cantidad, String idTransaccion);
String obtenerEstadoTransaccion(String idTransaccion);
}
// Implementación 1: Tarjeta de Crédito
class ProcesadorTarjetaCredito implements ProcesadorPago {
private String numTarjeta;
private String nombreTitular;
private Map<String, String> estadoTransacciones = new HashMap<>();
public ProcesadorTarjetaCredito(String numTarjeta, String nombreTitular) {
this.numTarjeta = numTarjeta;
this.nombreTitular = nombreTitular;
}
@Override
public boolean procesarPago(double cantidad) {
// Detalles complejos de conexión a banco, encriptación, etc.
System.out.println("Procesando pago de " + cantidad + " con tarjeta");
String idTransaccion = "TRX-" + System.currentTimeMillis();
estadoTransacciones.put(idTransaccion, "COMPLETADO");
return true; // Simplificado para ejemplo
}
@Override
public boolean reembolsar(double cantidad, String idTransaccion) {
System.out.println("Reembolsando " + cantidad);
estadoTransacciones.put(idTransaccion, "REEMBOLSADO");
return true;
}
@Override
public String obtenerEstadoTransaccion(String idTransaccion) {
return estadoTransacciones.getOrDefault(idTransaccion, "NO ENCONTRADO");
}
}
// Implementación 2: PayPal
class ProcesadorPayPal implements ProcesadorPago {
private String emailPayPal;
// ... implementación específica de PayPal
}
// Implementación 3: Criptomoneda
class ProcesadorCripto implements ProcesadorPago {
private String direccionWallet;
// ... implementación específica de cripto
}
// El cliente no necesita conocer detalles internos
class CarritoCompras {
private double total;
private ProcesadorPago procesador;
public CarritoCompras(ProcesadorPago procesador) {
this.procesador = procesador;
}
public void finalizarCompra(double monto) {
if (procesador.procesarPago(monto)) {
System.out.println("Compra completada exitosamente");
} else {
System.out.println("Error en el pago");
}
}
}
Beneficios:
- Simplicidad para el usuario: Solo interactúa con interfaz simple
- Flexibilidad: Cambiar procesador no requiere cambiar cliente
- Mantenibilidad: Detalles complejos aislados
- Escalabilidad: Agregar nuevos procesadores es trivial
3.5 Pilar 4: Polimorfismo - Múltiples Formas
Definición técnica: Polimorfismo significa "muchas formas". Permite que objetos de diferentes clases sean tratados uniformemente a través de una interfaz común, pero responda cada uno según su tipo.
Tres Tipos de Polimorfismo:
A) Polimorfismo de Subtipado (Tiempo de Ejecución):
List<Vehiculo> vehiculos = new ArrayList<>();
vehiculos.add(new Automovil(...));
vehiculos.add(new Motocicleta(...));
vehiculos.add(new Camion(...));
// Mismo código, comportamiento diferente según tipo
for (Vehiculo v : vehiculos) {
v.acelerar(); // Cada uno acelera diferente
v.frenar(); // Cada uno frena diferente
}
B) Polimorfismo de Sobrecarga (Tiempo de Compilación):
class Calculadora {
// Múltiples versiones del mismo método
public int sumar(int a, int b) {
return a + b;
}
public double sumar(double a, double b) {
return a + b;
}
public int sumar(int a, int b, int c) {
return a + b + c;
}
public String sumar(String a, String b) {
return a + b; // Concatenación
}
}
Calculadora calc = new Calculadora();
int res1 = calc.sumar(5, 3); // int + int
double res2 = calc.sumar(5.5, 3.2); // double + double
String res3 = calc.sumar("Hola", "Mundo"); // String concatenation
C) Polimorfismo de Parámetrico (Genéricos):
// Clase genérica que funciona con cualquier tipo
class Contenedor<T> {
private T contenido;
public void establecer(T valor) {
this.contenido = valor;
}
public T obtener() {
return contenido;
}
}
// Uso con diferentes tipos
Contenedor<String> contenedorString = new Contenedor<>();
contenedorString.establecer("Hola");
String valor1 = contenedorString.obtener();
Contenedor<Integer> contenedorInt = new Contenedor<>();
contenedorInt.establecer(42);
Integer valor2 = contenedorInt.obtener();
Contenedor<Vehiculo> contenedorVehiculo = new Contenedor<>();
contenedorVehiculo.establecer(new Automovil(...));
Vehiculo valor3 = contenedorVehiculo.obtener();
4. Programación Orientada a Eventos: Arquitectura Moderna
4.1 Concepto y Fundamentales
Definición técnica: La programación orientada a eventos es un paradigma donde el flujo del programa está determinado por eventos (acciones del usuario, cambios de estado, mensajes) que son capturados y procesados por manejadores de eventos.
4.2 Componentes Clave
Event Source (Productor de Eventos):
Genera eventos cuando algo importante sucede. Ej: Click del usuario, cambio de estado de base de datos, llegada de mensaje.
Event Dispatcher (Distribuidor):
Componente central que recibe eventos y los enruta a listeners interesados.
Event Listener (Consumidor):
Código registrado que se ejecuta cuando un evento ocurre.
4.3 Ejemplo Práctico - Sistema de Notificaciones de E-Commerce
// Definición de eventos
class EventoPedido {
public enum Tipo { CREADO, PAGADO, ENVIADO, ENTREGADO, CANCELADO }
private String idPedido;
private Tipo tipo;
private LocalDateTime timestamp;
private Map<String, Object> datos;
public EventoPedido(String idPedido, Tipo tipo, Map<String, Object> datos) {
this.idPedido = idPedido;
this.tipo = tipo;
this.timestamp = LocalDateTime.now();
this.datos = datos;
}
// Getters
public String getIdPedido() { return idPedido; }
public Tipo getTipo() { return tipo; }
public LocalDateTime getTimestamp() { return timestamp; }
public Map<String, Object> getDatos() { return datos; }
}
// Listener para enviar emails
class ListenerEmail implements ListenerEventoPedido {
@Override
public void alEvento(EventoPedido evento) {
switch(evento.getTipo()) {
case CREADO:
enviarEmail("Pedido confirmado", evento.getDatos().get("email"));
break;
case PAGADO:
enviarEmail("Pago recibido", evento.getDatos().get("email"));
break;
case ENVIADO:
enviarEmail("Pedido en camino", evento.getDatos().get("email"));
break;
case ENTREGADO:
enviarEmail("Pedido entregado", evento.getDatos().get("email"));
break;
}
}
private void enviarEmail(String asunto, Object email) {
System.out.println("📧 Email enviado a " + email + ": " + asunto);
}
}
// Listener para actualizar inventario
class ListenerInventario implements ListenerEventoPedido {
@Override
public void alEvento(EventoPedido evento) {
if (evento.getTipo() == EventoPedido.Tipo.CREADO) {
System.out.println("📦 Actualizando inventario para pedido " + evento.getIdPedido());
// Restar del inventario
} else if (evento.getTipo() == EventoPedido.Tipo.CANCELADO) {
System.out.println("📦 Devolviendo items al inventario");
// Sumar al inventario
}
}
}
// Listener para logging
class ListenerAuditoria implements ListenerEventoPedido {
@Override
public void alEvento(EventoPedido evento) {
System.out.println("[AUDIT] " + evento.getTimestamp() + " - " +
evento.getTipo() + " - Pedido: " + evento.getIdPedido());
}
}
// Dispatcher (Centro del evento)
class DispatcherEventoPedido {
private List<ListenerEventoPedido> listeners = new ArrayList<>();
public void registrarListener(ListenerEventoPedido listener) {
listeners.add(listener);
}
public void emitirEvento(EventoPedido evento) {
System.out.println("\n▶️ Evento: " + evento.getTipo());
for (ListenerEventoPedido listener : listeners) {
listener.alEvento(evento);
}
}
}
interface ListenerEventoPedido {
void alEvento(EventoPedido evento);
}
// Uso del sistema
public class SistemaEventosPedidos {
public static void main(String[] args) {
DispatcherEventoPedido dispatcher = new DispatcherEventoPedido();
// Registrar listeners
dispatcher.registrarListener(new ListenerEmail());
dispatcher.registrarListener(new ListenerInventario());
dispatcher.registrarListener(new ListenerAuditoria());
// Crear un pedido y emitir eventos
Map<String, Object> datosCliente = new HashMap<>();
datosCliente.put("email", "cliente@example.com");
datosCliente.put("items", 3);
// Flujo de eventos típico
dispatcher.emitirEvento(new EventoPedido("PED-001", EventoPedido.Tipo.CREADO, datosCliente));
dispatcher.emitirEvento(new EventoPedido("PED-001", EventoPedido.Tipo.PAGADO, datosCliente));
dispatcher.emitirEvento(new EventoPedido("PED-001", EventoPedido.Tipo.ENVIADO, datosCliente));
dispatcher.emitirEvento(new EventoPedido("PED-001", EventoPedido.Tipo.ENTREGADO, datosCliente));
}
}
Salida esperada:
▶️ Evento: CREADO 📧 Email enviado a cliente@example.com: Pedido confirmado 📦 Actualizando inventario para pedido PED-001 [AUDIT] 2025-10-26 19:30:00 - CREADO - Pedido: PED-001 ▶️ Evento: PAGADO 📧 Email enviado a cliente@example.com: Pago recibido [AUDIT] 2025-10-26 19:30:01 - PAGADO - Pedido: PED-001 ▶️ Evento: ENVIADO 📧 Email enviado a cliente@example.com: Pedido en camino [AUDIT] 2025-10-26 19:30:02 - ENVIADO - Pedido: PED-001 ▶️ Evento: ENTREGADO 📧 Email enviado a cliente@example.com: Pedido entregado [AUDIT] 2025-10-26 19:30:03 - ENTREGADO - Pedido: PED-001
4.4 Arquitecturas Avanzadas: Event Sourcing y CQRS
Event Sourcing:
Concepto: En lugar de almacenar solo el estado actual, se almacena una secuencia inmutable de eventos que representa todo lo que ha sucedido. El estado actual se reconstruye reproduciendo los eventos.
Comparación: Estado vs Event Sourcing
Enfoque Tradicional (State-Based):
Base de Datos: { id_cuenta: 123, saldo: 5000 }
Problema: Si el saldo es incorrecto, ¿cómo sé qué pasó?
Event Sourcing (Event-Based):
Event Store: [
{ id: 1, evento: "CuentaAbierta", saldo_inicial: 1000 },
{ id: 2, evento: "Deposito", cantidad: 2000 },
{ id: 3, evento: "Retiro", cantidad: 500 },
{ id: 4, evento: "InteresAplicado", cantidad: 50 }
]
Ventaja: Historial completo. Puedo reconstruir estado en cualquier punto tiempo.
CQRS (Command Query Responsibility Segregation):
Concepto: Separa operaciones de lectura (queries) de escritura (commands). Bases de datos optimizadas diferentemente para cada caso.
// COMANDO: Escribe en Event Store
class ComandoDepositar implements Comando {
private String idCuenta;
private double cantidad;
public void ejecutar(RepositorioEventos repo) {
// Validar
if (cantidad <= 0) throw new InvalidArgumentException();
// Crear evento
EventoDeposito evento = new EventoDeposito(idCuenta, cantidad);
// Guardar en Event Store
repo.guardarEvento(evento);
// Notificar listeners para actualizar proyecciones
publicarEvento(evento);
}
}
// QUERY: Lee desde proyección optimizada
class ConsultaSaldoActual implements Query {
private String idCuenta;
public double ejecutar(RepositorioCuentas repo) {
// Leer de base de datos optimizada para lectura
return repo.obtenerSaldoActual(idCuenta);
}
}
// Arquitectura
class SistemaBancarioCQRS {
private RepositorioEventos eventStore; // Write Model
private RepositorioCuentas cuentasproyection; // Read Model
private ProcessadorEventos procesador;
public SistemaBancarioCQRS() {
this.eventStore = new EventStore();
this.cuentasProjection = new CuentasProjection();
this.procesador = new ProcessadorEventos();
// Procesador actualiza proyecciones cuando hay eventos
procesador.registrarListener(new ActualizadorProyecciones(cuentasProjection));
}
// Ejecutar comando (escribir)
public void ejecutarComando(Comando cmd) {
cmd.ejecutar(eventStore);
}
// Ejecutar query (leer)
public Object ejecutarQuery(Query q) {
return q.ejecutar(cuentasProjection);
}
}
Beneficios de Event Sourcing + CQRS:
- Auditoría completa: Todo lo que pasó está en el event store
- Reconstrucción: Puedo reconstruir estado en cualquier momento
- Escalabilidad: Read models optimizadas para lectura, separadas de write
- Debugging: Puedo reproducir exactamente qué pasó
- Flexibilidad: Cambiar estructura de datos de lectura sin perder historial
5. Programación Orientada a Aspectos (AOP): Separación de Preocupaciones Transversales
5.1 Problema que AOP Resuelve
El Problema de Código Disperso
Funcionalidades como logging, seguridad, transacciones, etc. se necesitan en MÚLTIPLES MÉTODOS sin ser parte de la lógica de negocio.
// ANTES: Sin AOP - Código disperso y repetido
class ProcesadorPedidos {
public Pedido crearPedido(DatosPedido datos) {
// Verificar seguridad
if (!tienePermiso(Usuario.actual())) {
throw new SecurityException("No tienes permiso");
}
// Log entrada
logger.info("Iniciando creación de pedido");
// Iniciar transacción
Transaction tx = startTransaction();
try {
// LÓGICA REAL DE NEGOCIO
Pedido pedido = new Pedido(datos);
repositorio.guardar(pedido);
// Log salida
logger.info("Pedido creado: " + pedido.getId());
// Commit transacción
tx.commit();
return pedido;
} catch (Exception e) {
tx.rollback();
logger.error("Error creando pedido: " + e);
throw e;
}
}
public void procesarPago(String idPedido, double monto) {
// Verificar seguridad
if (!tienePermiso(Usuario.actual())) {
throw new SecurityException("No tienes permiso");
}
// Log entrada
logger.info("Iniciando pago para pedido: " + idPedido);
// Iniciar transacción
Transaction tx = startTransaction();
try {
// LÓGICA REAL DE NEGOCIO
Pedido pedido = repositorio.obtener(idPedido);
pedido.procesarPago(monto);
repositorio.guardar(pedido);
// Log salida
logger.info("Pago procesado");
// Commit transacción
tx.commit();
} catch (Exception e) {
tx.rollback();
logger.error("Error procesando pago: " + e);
throw e;
}
}
// ... más métodos con el mismo patrón repetido
}
Problemas: Código repetido, difícil mantener, lógica de negocio oscurecida, cambios requieren editar múltiples métodos.
5.2 Solución con AOP
Conceptos Clave Detallados:
| Concepto | Definición Técnica | Ejemplo |
|---|---|---|
| Aspecto | Módulo que encapsula una preocupación transversal | @Aspect AspectoDeSeguridadYLogging |
| Join Point | Punto de ejecución donde aspecto puede aplicarse | Llamada a método procesar() |
| Pointcut | Expresión que especifica qué join points | @Pointcut("execution(public * *.procesar(..))") |
| Advice | Código que se ejecuta en join point | Antes, Después, Alrededor del método |
// DESPUÉS: Con AOP - Separación clara
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// Aspecto que agrupa preocupaciones transversales
@Aspect
@Component
public class AspectoDeSeguridadYTransaccional {
// Pointcut que especifica DÓNDE aplicar el aspecto
@Pointcut("execution(public * com.ecommerce..*Procesador.*(..))")
public void operacionesCriticas() {}
// BEFORE: Se ejecuta ANTES del método
@Before("operacionesCriticas()")
public void verificarSeguridad(JoinPoint jp) {
if (!tienePermiso(Usuario.actual())) {
throw new SecurityException("Acceso denegado");
}
System.out.println("[SEGURIDAD] Verificado para: " + jp.getSignature());
}
// BEFORE: Log de entrada
@Before("operacionesCriticas()")
public void logEntrada(JoinPoint jp) {
System.out.println("[LOG] Iniciando: " + jp.getSignature());
System.out.println("[LOG] Argumentos: " + Arrays.toString(jp.getArgs()));
}
// AROUND: Se ejecuta ALREDEDOR del método (permite controlar)
@Around("operacionesCriticas()")
public Object gestionarTransaccion(ProceedingJoinPoint pjp) throws Throwable {
Transaction tx = startTransaction();
try {
System.out.println("[TX] Iniciando transacción");
Object result = pjp.proceed(); // Ejecutar método original
tx.commit();
System.out.println("[TX] Transacción completada");
return result;
} catch (Exception e) {
tx.rollback();
System.out.println("[TX] Transacción revertida por error");
throw e;
}
}
// AFTER_RETURNING: Se ejecuta DESPUÉS si todo va bien
@AfterReturning(pointcut = "operacionesCriticas()", returning = "result")
public void logExito(JoinPoint jp, Object result) {
System.out.println("[LOG] Exitoso: " + jp.getSignature());
System.out.println("[LOG] Resultado: " + result);
}
// AFTER_THROWING: Se ejecuta si hay excepción
@AfterThrowing(pointcut = "operacionesCriticas()", throwing = "ex")
public void logError(JoinPoint jp, Exception ex) {
System.out.println("[ERROR] En: " + jp.getSignature());
System.out.println("[ERROR] Excepción: " + ex.getMessage());
}
}
// AHORA: Código de negocio LIMPIO y SIN distracciones
@Component
public class ProcesadorPedidosConAOP {
// MÁS LIMPIO: Sin seguridad, sin transacciones, sin logs
// El aspecto se aplica automáticamente
public Pedido crearPedido(DatosPedido datos) {
// SOLO lógica de negocio
Pedido pedido = new Pedido(datos);
repositorio.guardar(pedido);
return pedido;
}
public void procesarPago(String idPedido, double monto) {
// SOLO lógica de negocio
Pedido pedido = repositorio.obtener(idPedido);
pedido.procesarPago(monto);
repositorio.guardar(pedido);
}
}
Comparación Visual:
SIN AOP (Tradicional)
- Código mezclado
- Difícil de cambiar
- Duplicación masiva
- Lógica oscurecida
CON AOP (Moderno)
- Separación clara
- Fácil de modificar
- No hay duplicación
- Lógica enfocada
5.3 Casos de Uso Prácticos de AOP
1. Logging y Monitoreo:
Caso: Sistema de Salud SAS
Necesitamos loguear TODAS las operaciones críticas de pacientes. Con AOP, un solo aspecto monitorea automáticamente todas las operaciones relacionadas sin duplicar código.
2. Seguridad y Control de Acceso:
Caso: Acceso a Expedientes Médicos
Solo doctores pueden ver expedientes. AOP verifica permisos automáticamente en cualquier método que intente acceder a datos sensibles.
3. Transacciones de Base de Datos:
Caso: Transferencia de Datos
Las transacciones deben ser ACID. AOP envuelve automáticamente operaciones críticas en transacciones sin que el desarrollador lo especifique explícitamente.
4. Caching:
Caso: Búsqueda de Pacientes
Búsquedas frecuentes deben cachearse. AOP intercepta llamadas y retorna resultado en cache si existe, sin cambiar código original.
6. Diseño Centrado en Usuario (UCD): Profundización
6.1 Principios Fundamentales Detallados
| Principio | Aplicación Práctica | En Sistema SAS |
|---|---|---|
| Énfasis en Usuario | Investigar necesidades reales, no asumir | Entrevistar médicos sobre flujo de trabajo actual |
| Iteración Continua | Prototipo → Usuario → Feedback → Mejora | Mostrar versiones a médicos regularmente |
| Accesibilidad | Funciona para todos (incluyendo discapacitados) | Alto contraste para médicos con discapacidad visual |
| Eficiencia | Menos clics, operaciones rápidas | Acceder a expediente en máximo 3 clics |
6.2 Métodos de Investigación UCD
- Entrevistas: Hablar con usuarios actuales sobre tareas, frustraciones, objetivos
- Observación: Ver cómo realmente trabajan (no cómo dicen que trabajan)
- Focus Groups: Discusiones de grupo sobre propuestas
- Encuestas: Recopilar datos cuantitativos
- Pruebas de Usabilidad: Usuarios reales usando prototipos
- Personas: Crear arquetipos de usuarios
- Scenarios: Historias de cómo se usaría el sistema
7. Test-Driven Development (TDD): Profundización Técnica
7.1 Ciclo Red-Green-Refactor Detallado
Ejemplo Paso a Paso: Sistema de Cálculo de Descuentos
// ========== FASE 1: RED (Rojo) ==========
// Escribo una prueba que FALLA porque la función no existe
@Test
public void debeCalcularDescuentoDelDiezPorCiento() {
// Arrange (Preparar)
CalculadorDescuentos calc = new CalculadorDescuentos();
// Act (Actuar)
double resultado = calc.calcularDescuento(100, 10); // 10% descuento
// Assert (Afirmar)
assertEquals(90, resultado, 0.01); // 100 - 10% = 90
}
// Ejecuto: FALLA - "CalculadorDescuentos no existe"
// ========== FASE 2: GREEN (Verde) ==========
// Escribo código MÍNIMO para pasar la prueba
public class CalculadorDescuentos {
public double calcularDescuento(double monto, double porcentajeDescuento) {
return monto - (monto * porcentajeDescuento / 100);
}
}
// Ejecuto: PASA ✓
// ========== FASE 3: REFACTOR (Refactorizar) ==========
// Mejoro el código manteniendo funcionalidad
public class CalculadorDescuentos {
private static final double FACTOR_PORCENTAJE = 100.0;
public double calcularDescuento(double monto, double porcentajeDescuento) {
validarEntrada(monto, porcentajeDescuento);
return monto * (1 - porcentajeDescuento / FACTOR_PORCENTAJE);
}
private void validarEntrada(double monto, double porcentaje) {
if (monto < 0) throw new IllegalArgumentException("Monto no puede ser negativo");
if (porcentaje < 0 || porcentaje > 100) throw new IllegalArgumentException("Porcentaje debe estar entre 0-100");
}
}
// Ejecuto: SIGUE PASANDO ✓
// ========== REPETIR CICLO ==========
// Escribo nueva prueba para caso específico
@Test
public void debeManejardescuentoDelCienPorCiento() {
CalculadorDescuentos calc = new CalculadorDescuentos();
double resultado = calc.calcularDescuento(100, 100);
assertEquals(0, resultado, 0.01); // 100% descuento = 0
}
// FALLA inicialmente...después GREEN...después REFACTOR...
// Ciclo continúa...
7.2 TDD en Equipo: Mejores Prácticas
- Cobertura de Pruebas: Mínimo 80% líneas de código
- Nombres Descriptivos: El nombre de la prueba describe exactamente qué prueba
- AAA Pattern: Arrange-Act-Assert en cada prueba
- Pruebas Independientes: Cada prueba debe ser ejecutable sola
- Sin Lógica en Pruebas: Las pruebas deben ser simples y obvias
- Datos de Prueba Realistas: Usar datos que representen casos reales
7.3 Impacto en Desarrollo SAS
Caso Real: Sistema de Recetas Médicas
Sin TDD: Desarrollo rápido inicial → Defectos en producción → Médicos sin poder dar recetas → Crisis.
Con TDD: Desarrollo más lento inicial pero defectos detectados antes → Calidad garantizada → Confianza en el sistema.
8. Modularidad y SOLID: Principios Prácticos
8.1 SOLID Detallado
S - Single Responsibility (Una Sola Responsabilidad):
// ❌ VIOLACIÓN de SRP: Una clase hace demasiado
class GestorPacientes {
public void crearPaciente(Paciente p) { /* guardar */ }
public void enviarEmail(String email) { /* SMTP */ }
public void generarReporte(List<Paciente> p) { /* PDF */ }
public void procesarPago(double monto) { /* Stripe */ }
}
// ✅ CUMPLIMIENTO de SRP: Cada clase una responsabilidad
class RepositorioPacientes {
public void guardar(Paciente p) { /* BD */ }
}
class ServicioEmail {
public void enviar(String email) { /* SMTP */ }
}
class GeneradorReportes {
public PDF generar(List<Paciente> p) { /* PDF */ }
}
class ProcesadorPagos {
public void procesar(double monto) { /* Stripe */ }
}
O - Open/Closed (Abierto para Extensión, Cerrado para Modificación):
// ❌ VIOLACIÓN de OCP: Si agrego nuevo descuento, debo modificar clase
class CalculadorPrecioFinal {
public double calcular(Pedido pedido, String tipoCliente) {
double precio = pedido.getPrecio();
if (tipoCliente.equals("VIP")) {
return precio * 0.8; // 20% descuento
} else if (tipoCliente.equals("REGULAR")) {
return precio * 0.95; // 5% descuento
} else if (tipoCliente.equals("NUEVO")) {
return precio; // Sin descuento
}
// Si agrego "GOBIERNO", ¡debo editar esta clase!
}
}
// ✅ CUMPLIMIENTO de OCP: Extensible sin modificar
interface EstrategiaDescuento {
double aplicarDescuento(double precio);
}
class DescuentoVIP implements EstrategiaDescuento {
@Override
public double aplicarDescuento(double precio) {
return precio * 0.8; // 20%
}
}
class DescuentoRegular implements EstrategiaDescuento {
@Override
public double aplicarDescuento(double precio) {
return precio * 0.95; // 5%
}
}
class DescuentoGobierno implements EstrategiaDescuento {
@Override
public double aplicarDescuento(double precio) {
return precio * 0.5; // 50%
}
}
class CalculadorPrecioFinal {
public double calcular(Pedido pedido, EstrategiaDescuento estrategia) {
return estrategia.aplicarDescuento(pedido.getPrecio());
// Agregar nuevo descuento: SIN MODIFICAR esta clase
}
}
9. Cuestionario de Evaluación (15 Preguntas)
Pregunta 1: ¿Cuál es el paradigma que utiliza procedimientos y funciones con control de flujo explícito?
La programación estructurada utiliza procedimientos, funciones y control de flujo mediante if/else y bucles, eliminando el GOTO del código.
Pregunta 2: ¿Cuáles son los 4 pilares de la Programación Orientada a Objetos?
Los 4 pilares de POO son: Encapsulación (agrupar datos y métodos), Herencia (reutilización), Abstracción (ocultar complejidad), Polimorfismo (múltiples formas).
Pregunta 3: ¿Qué es AOP (Programación Orientada a Aspectos)?
AOP modulariza funcionalidades transversales (logging, seguridad, transacciones) que afectan múltiples partes del código, separándolas del código principal.
Pregunta 4: ¿Cuál es el ciclo fundamental de TDD?
TDD sigue el ciclo Red (prueba falla) - Green (código mínimo para pasar) - Refactor (mejorar código). Este ciclo se repite continuamente.
Pregunta 5: ¿Qué es User-Centered Design (UCD)?
UCD es un enfoque que pone al usuario final en el centro del proceso de diseño, investigando necesidades, evaluando con usuarios reales e iterando continuamente.
Pregunta 6: ¿Cuál es una ventaja principal de la Programación Orientada a Objetos?
POO permite crear módulos independientes reutilizables, facilita el mantenimiento y escalabilidad del código, lo que es ideal para proyectos grandes.
Pregunta 7: ¿Qué caracteriza a la Programación Orientada a Eventos?
La programación orientada a eventos se basa en un event loop que responde a eventos (clics, cambios de estado, mensajes) con handlers asincronía.
Pregunta 8: ¿Qué es un "Aspecto" en AOP?
En AOP, un aspecto es un módulo que encapsula funcionalidades transversales que afectan múltiples partes del código, como logging o seguridad.
Pregunta 9: ¿Cuál es el principal beneficio de la Generación Automática de Código?
La generación automática acelera el desarrollo, asegura consistencia en patrones y reduce errores al generar código desde especificaciones.
Pregunta 10: ¿Qué principio SOLID significa que cada módulo tiene una única responsabilidad?
Single Responsibility Principle (SRP) establece que cada módulo, clase o función debe tener una única razón para cambiar, es decir, una responsabilidad.
Pregunta 11: ¿Cuál es una característica de la Programación Visual?
La programación visual permite crear programas manipulando elementos gráficos en interfaz drag-and-drop, con menor curva de aprendizaje.
Pregunta 12: ¿Cuál es el costo de implementar TDD?
TDD requiere inversión inicial en tiempo para escribir pruebas, pero esa inversión se recupera por reducción de defectos y mantenimiento más fácil.
Pregunta 13: ¿Cuáles son las 3 etapas principales del proceso UCD?
UCD incluye: 1) Análisis (investigar tareas usuario), 2) Diseño (crear soluciones), 3) Evaluación (pruebas con usuarios reales).
Pregunta 14: ¿Qué proporciona modularidad en un sistema?
Modularidad permite dividir un sistema en módulos independientes que pueden desarrollarse, testarse y mantenerse separadamente, facilitando mantenimiento.
Pregunta 15: ¿Cuál es una tendencia moderna en programación?
Tendencias modernas incluyen: IA para generación de código (GitHub Copilot), microservicios, arquitecturas reactivas, programación asincrónica y FaaS.
10. Referencias y Recursos
Libros Fundamentales
- Gang of Four. Design Patterns: Elements of Reusable Object-Oriented Software (1994)
- Beck, K. Test Driven Development: By Example (2003)
- Martin, R.C. Clean Code (2008)
- Pressman, R.S. Software Engineering: A Practitioner's Approach (9ª ed, 2021)
- Sommerville, I. Software Engineering (10ª ed, 2020)
Patrones y Arquitecturas
- Microservices Patterns - Chris Richardson
- Enterprise Integration Patterns - Hohpe & Woolf
- Building Microservices - Sam Newman
Estándares Aplicables a SAS
- MÉTRICA Versión 3 - Metodología oficial España
- ISO 9001:2015 - Calidad
- ISO/IEC/IEEE 29119 - Pruebas de Software
