OPE 2025 TFA INF. Tema 45. Programación. Evolución. Paradigmas de la programación. Programación estructurada. Orientación a objetos. Orientación a eventos. Orientación a aspectos. Programación visual. Generación automática de código. Modularidad y reutilización de componentes. Diseño centrado en el usuario. Desarrollo orientado a pruebas (TDD). Nuevas tendencias.

Servicio Andaluz de Salud EXAMEN INFORMÁTICA JUNTA DE ANDALUCÍA Exámenes SAS 2025 TFA INF (P) TFA INFORMÁTICA
Tema 45 – Programación Paradigmas PROFUNDIZADO – TFA-STI SAS

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».

Revolucionarios de esta era: Lenguajes como C (1972) y Pascal (1971) permitían escribir código legible, estructurado y reutilizable. Las funciones se convirtieron en bloques de construcción principales.

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

  1. Entrevistas: Hablar con usuarios actuales sobre tareas, frustraciones, objetivos
  2. Observación: Ver cómo realmente trabajan (no cómo dicen que trabajan)
  3. Focus Groups: Discusiones de grupo sobre propuestas
  4. Encuestas: Recopilar datos cuantitativos
  5. Pruebas de Usabilidad: Usuarios reales usando prototipos
  6. Personas: Crear arquetipos de usuarios
  7. 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?

A) Orientado a Objetos
B) Estructurado (Imperativo)
C) Orientado a Eventos
D) Funcional
Respuesta Correcta: B

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?

A) Abstracción, Herencia, Encapsulación, Modularidad
B) Encapsulación, Herencia, Abstracción, Polimorfismo
C) Polimorfismo, Herencia, Objetos, Clases
D) Métodos, Propiedades, Eventos, Interfaces
Respuesta Correcta: B

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)?

A) Un paradigma que modulariza preocupaciones transversales como logging y seguridad
B) Un lenguaje de programación
C) Un tipo de base de datos
D) Una metodología de pruebas
Respuesta Correcta: A

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?

A) Código - Prueba - Refactor
B) Red - Green - Refactor
C) Prueba - Código - Validar
D) Diseño - Implementación - Pruebas
Respuesta Correcta: B

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)?

A) Diseño centrado en usar muchas características
B) Filosofía que prioriza necesidades del usuario final en diseño y desarrollo
C) Una herramienta de diseño gráfico
D) Un lenguaje de programación
Respuesta Correcta: B

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?

A) Es más rápida que otras paradigmas
B) Modularidad, reutilización y mejor mantenibilidad
C) Usa menos memoria que programación estructurada
D) No requiere pruebas
Respuesta Correcta: B

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?

A) Uso de objetos y herencia
B) Programa responde a eventos externos en lugar de flujo secuencial
C) Uso de estructuras y procedimientos
D) Basada en reglas lógicas
Respuesta Correcta: B

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?

A) Un método de una clase
B) Módulo que encapsula una preocupación transversal (logging, seguridad, etc.)
C) Una tipo de parámetro
D) Una excepción de error
Respuesta Correcta: B

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?

A) Elimina la necesidad de programadores
B) Aceleración del desarrollo y reducción de errores manuales
C) Hace el código más complejo
D) Solo funciona con un lenguaje
Respuesta Correcta: B

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?

A) Open/Closed
B) Single Responsibility
C) Liskov Substitution
D) Dependency Inversion
Respuesta Correcta: B

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?

A) Solo usa código textual
B) Manipula elementos gráficos drag-and-drop para crear programas
C) Es más rápida que textual
D) No genera código ejecutable
Respuesta Correcta: B

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?

A) Prácticamente nulo
B) Incremento inicial de tiempo, pero recuperable a largo plazo
C) Es gratis sin costos
D) Imposible calcular
Respuesta Correcta: B

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?

A) Codificación, Pruebas, Despliegue
B) Análisis, Diseño, Evaluación
C) Planificación, Ejecución, Control
D) Requisitos, Especificación, Implementación
Respuesta Correcta: B

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?

A) Mayor complejidad del código
B) División en módulos independientes desarrollables por separado
C) Eliminación de todas las pruebas
D) Uso de un solo archivo
Respuesta Correcta: B

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?

A) Volver a COBOL exclusivamente
B) Generación de código asistida por IA, microservicios, programación asincrónica
C) Eliminar todas las pruebas
D) Usar solo monolitos
Respuesta Correcta: B

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

  1. Gang of Four. Design Patterns: Elements of Reusable Object-Oriented Software (1994)
  2. Beck, K. Test Driven Development: By Example (2003)
  3. Martin, R.C. Clean Code (2008)
  4. Pressman, R.S. Software Engineering: A Practitioner's Approach (9ª ed, 2021)
  5. Sommerville, I. Software Engineering (10ª ed, 2020)

Patrones y Arquitecturas

  1. Microservices Patterns - Chris Richardson
  2. Enterprise Integration Patterns - Hohpe & Woolf
  3. Building Microservices - Sam Newman

Estándares Aplicables a SAS

  1. MÉTRICA Versión 3 - Metodología oficial España
  2. ISO 9001:2015 - Calidad
  3. ISO/IEC/IEEE 29119 - Pruebas de Software

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *