Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Programación Orientada a Objetos

SOLID

Fundamentos del Diseño Orientado a Objetos de Calidad

Universidad Nacional de Rio Negro - Sede Andina

OOP 6: Principios SOLID

En los capítulos anteriores construimos objetos, establecimos relaciones, exploramos herencia y polimorfismo, definimos contratos (Diseño por Contratos), y estudiamos patrones de diseño (Parte 4: Patrones de Diseño). Pero, ¿cómo sabemos si nuestro diseño es bueno? ¿Qué características debe tener un sistema orientado a objetos para ser mantenible, extensible y robusto?

Los principios SOLID son cinco directrices fundamentales que guían el diseño de software orientado a objetos hacia sistemas de alta calidad. Fueron recopilados y popularizados por Robert C. Martin (Uncle Bob) a principios de los 2000, aunque cada principio tiene raíces más antiguas. Estos principios también ayudan a identificar y corregir Code Smells: Detectando Problemas en el código.


Introducción: ¿Por qué SOLID?

El Costo del Mal Diseño

Considerá un sistema que crece sin principios claros de diseño:

Año 1: "¡Funciona! El código es simple."
Año 2: "Agregar features toma más tiempo..."
Año 3: "Cada cambio rompe algo en otro lado."
Año 4: "Nadie entiende este código."
Año 5: "Necesitamos reescribir todo desde cero."

Este patrón, conocido como degradación del diseño, ocurre cuando el código acumula deuda técnica sin control. Los síntomas incluyen:

SOLID como Antídoto

Los principios SOLID atacan directamente estos síntomas:

PrincipioCombate
S (Single Responsibility)Rigidez
O (Open/Closed)Fragilidad
L (Liskov Substitution)Fragilidad, Inmovilidad
I (Interface Segregation)Rigidez, Inmovilidad
D (Dependency Inversion)Rigidez, Inmovilidad

S: Principio de Responsabilidad Única

Definición

La formulación original habla de “razón para cambiar” en lugar de “responsabilidad” porque es más precisa. Una razón para cambiar representa a un actor o stakeholder que podría solicitar modificaciones.

Ejemplo: Violación del SRP

Considerá una clase Empleado típica:

public class Empleado {
    private String nombre;
    private double salarioBase;
    private int horasTrabajadas;
    
    // Calcula el salario (usado por Contabilidad)
    public double calcularSalario() {
        return salarioBase + (horasTrabajadas * 50);
    }
    
    // Genera reporte para RRHH
    public String generarReporteRRHH() {
        return "Empleado: " + nombre + 
               "\nHoras: " + horasTrabajadas;
    }
    
    // Guarda en base de datos (usado por IT)
    public void guardarEnBaseDeDatos() {
        // Conexión a BD, SQL, etc.
    }
}

Esta clase tiene tres razones para cambiar:

  1. Contabilidad podría cambiar la fórmula de cálculo salarial

  2. RRHH podría modificar el formato del reporte

  3. IT podría cambiar el esquema de base de datos

Problemas de Esta Violación

  1. Acoplamiento no deseado: Un cambio pedido por Contabilidad podría afectar a RRHH o IT

  2. Compilación innecesaria: Modificar el reporte obliga a recompilar y redesplegar todo

  3. Conflictos de merge: Múltiples equipos editando el mismo archivo

Refactorización: Separar Responsabilidades

// Datos del empleado (estructura de datos pura)
public class Empleado {
    private String nombre;
    private double salarioBase;
    private int horasTrabajadas;
    
    // Solo getters/setters
    public String getNombre() { return nombre; }
    public double getSalarioBase() { return salarioBase; }
    public int getHorasTrabajadas() { return horasTrabajadas; }
}

// Responsabilidad: Contabilidad
public class CalculadorSalario {
    public double calcular(Empleado empleado) {
        return empleado.getSalarioBase() + 
               (empleado.getHorasTrabajadas() * 50);
    }
}

// Responsabilidad: RRHH
public class GeneradorReporteRRHH {
    public String generar(Empleado empleado) {
        return "Empleado: " + empleado.getNombre() + 
               "\nHoras: " + empleado.getHorasTrabajadas();
    }
}

// Responsabilidad: Persistencia
public class EmpleadoRepository {
    public void guardar(Empleado empleado) {
        // Lógica de persistencia
    }
}

Ahora cada clase tiene una única razón para cambiar:

¿Cuánta Separación es Suficiente?

El SRP no dice que cada clase debe tener un solo método. El criterio es razones para cambiar, no cantidad de código.

Sobre-aplicación (exceso de clases):

// ¡Demasiado granular!
public class NombreEmpleado { ... }
public class SalarioBaseEmpleado { ... }
public class HorasTrabajadasEmpleado { ... }

Sub-aplicación (clase monolítica):

// ¡Demasiadas responsabilidades!
public class SistemaEmpresarial {
    // Empleados, clientes, productos, ventas,
    // reportes, emails, notificaciones...
}

O: Principio Abierto/Cerrado

Definición

Esto significa que debemos poder agregar nuevo comportamiento sin modificar código existente. La técnica principal para lograrlo es la abstracción mediante interfaces o clases abstractas.

Ejemplo: Violación del OCP

Considerá un sistema de cálculo de áreas:

public class CalculadorArea {
    
    public double calcularAreaTotal(List<Object> figuras) {
        double total = 0;
        
        for (Object figura : figuras) {
            if (figura instanceof Rectangulo) {
                Rectangulo r = (Rectangulo) figura;
                total += r.getAncho() * r.getAlto();
                
            } else if (figura instanceof Circulo) {
                Circulo c = (Circulo) figura;
                total += Math.PI * c.getRadio() * c.getRadio();
                
            } else if (figura instanceof Triangulo) {
                Triangulo t = (Triangulo) figura;
                total += (t.getBase() * t.getAltura()) / 2;
            }
            // ¿Qué pasa si agregamos Hexágono, Trapecio, etc.?
        }
        
        return total;
    }
}

Cada vez que se agrega una nueva figura, hay que modificar CalculadorArea. La clase no está cerrada para modificación.

Problema: Agregar una nueva figura (Hexágono, Trapecio) requiere modificar CalculadorArea.

Refactorización: Usar Abstracción

// Abstracción: contrato que todas las figuras deben cumplir
public interface Figura {
    double calcularArea();
}

// Cada figura implementa su propio cálculo
public class Rectangulo implements Figura {
    private double ancho;
    private double alto;
    
    @Override
    public double calcularArea() {
        return ancho * alto;
    }
}

public class Circulo implements Figura {
    private double radio;
    
    @Override
    public double calcularArea() {
        return Math.PI * radio * radio;
    }
}

public class Triangulo implements Figura {
    private double base;
    private double altura;
    
    @Override
    public double calcularArea() {
        return (base * altura) / 2;
    }
}

// Calculador cerrado para modificación, abierto para extensión
public class CalculadorArea {
    
    public double calcularAreaTotal(List<Figura> figuras) {
        double total = 0;
        for (Figura figura : figuras) {
            total += figura.calcularArea();
        }
        return total;
    }
}

Ahora agregar un Hexagono no requiere modificar CalculadorArea:

Beneficio: Agregar Hexagono solo requiere crear la nueva clase, sin tocar CalculadorArea.

Mecanismos para Lograr OCP

1. Polimorfismo (el más común)
interface Exportador {
    void exportar(Documento doc);
}

class ExportadorPDF implements Exportador { ... }
class ExportadorWord implements Exportador { ... }
class ExportadorHTML implements Exportador { ... }  // Nuevo, sin modificar nada
2. Patrón Strategy
interface EstrategiaDescuento {
    double aplicar(double precio);
}

class SinDescuento implements EstrategiaDescuento { ... }
class DescuentoPorcentual implements EstrategiaDescuento { ... }
class DescuentoBlackFriday implements EstrategiaDescuento { ... }  // Nuevo
3. Patrón Template Method
abstract class ProcesadorArchivo {
    public final void procesar() {
        abrir();
        leerContenido();      // Hook para extensión
        procesarContenido();  // Hook para extensión
        cerrar();
    }
    
    protected abstract void leerContenido();
    protected abstract void procesarContenido();
}

El Problema de la Anticipación

OCP requiere anticipar qué partes del sistema cambiarán. Pero predecir el futuro es difícil.


L: Principio de Sustitución de Liskov

Definición

En términos más simples: si S es subtipo de T, entonces objetos de tipo T pueden ser sustituidos por objetos de tipo S sin que el programa se rompa.

El Ejemplo Clásico: Rectángulo y Cuadrado

Matemáticamente, un cuadrado es un rectángulo. Entonces, ¿Cuadrado debería heredar de Rectangulo?

public class Rectangulo {
    protected int ancho;
    protected int alto;
    
    public void setAncho(int ancho) {
        this.ancho = ancho;
    }
    
    public void setAlto(int alto) {
        this.alto = alto;
    }
    
    public int getArea() {
        return ancho * alto;
    }
}

public class Cuadrado extends Rectangulo {
    
    @Override
    public void setAncho(int ancho) {
        this.ancho = ancho;
        this.alto = ancho;  // ¡Mantener cuadrado!
    }
    
    @Override
    public void setAlto(int alto) {
        this.alto = alto;
        this.ancho = alto;  // ¡Mantener cuadrado!
    }
}

Parece razonable, pero viola LSP. Considerá este código cliente:

public void agrandarRectangulo(Rectangulo r) {
    int anchoOriginal = r.getAncho();
    r.setAlto(r.getAlto() + 10);
    
    // Postcondición esperada: el ancho no cambió
    assert r.getAncho() == anchoOriginal;  // ¡FALLA con Cuadrado!
}

Problema: Cuadrado viola las expectativas del cliente que usa Rectangulo.

Reglas de LSP

Para que un subtipo sea sustituible, debe cumplir:

1. Precondiciones no pueden fortalecerse

El subtipo no puede exigir más que el supertipo.

// Supertipo
class Procesador {
    // Precondición: valor >= 0
    void procesar(int valor) { ... }
}

// INCORRECTO: fortalece precondición
class ProcesadorEstricto extends Procesador {
    // Precondición: valor >= 10  ← ¡Más restrictiva!
    @Override
    void procesar(int valor) {
        if (valor < 10) throw new IllegalArgumentException();
    }
}

// CORRECTO: debilita o mantiene precondición
class ProcesadorFlexible extends Procesador {
    // Precondición: valor >= -100  ← Acepta más valores
    @Override
    void procesar(int valor) { ... }
}
2. Postcondiciones no pueden debilitarse

El subtipo debe garantizar al menos lo mismo que el supertipo.

// Supertipo
class Buscador {
    // Postcondición: retorna lista ordenada
    List<String> buscar(String query) { ... }
}

// INCORRECTO: debilita postcondición
class BuscadorRapido extends Buscador {
    // Postcondición: retorna lista (¿ordenada? a veces...)
    @Override
    List<String> buscar(String query) {
        // Retorna sin ordenar para ser más rápido
    }
}

// CORRECTO: fortalece postcondición
class BuscadorCompleto extends Buscador {
    // Postcondición: retorna lista ordenada + sin duplicados
    @Override
    List<String> buscar(String query) { ... }
}
3. Invariantes deben preservarse

Las condiciones que siempre son verdaderas en el supertipo también deben serlo en el subtipo.

// Invariante: saldo >= 0
class CuentaBancaria {
    protected double saldo;
    
    void retirar(double monto) {
        if (monto > saldo) throw new SaldoInsuficienteException();
        saldo -= monto;
    }
}

// INCORRECTO: viola invariante
class CuentaConDescubierto extends CuentaBancaria {
    @Override
    void retirar(double monto) {
        saldo -= monto;  // ¡Permite saldo negativo!
    }
}

Solución al Problema Rectángulo-Cuadrado

Hay varias formas de resolverlo:

Opción 1: Inmutabilidad
public final class Rectangulo {
    private final int ancho;
    private final int alto;
    
    public Rectangulo(int ancho, int alto) {
        this.ancho = ancho;
        this.alto = alto;
    }
    
    public Rectangulo conAncho(int nuevoAncho) {
        return new Rectangulo(nuevoAncho, this.alto);
    }
    
    public Rectangulo conAlto(int nuevoAlto) {
        return new Rectangulo(this.ancho, nuevoAlto);
    }
}

public final class Cuadrado {
    private final int lado;
    
    public Cuadrado(int lado) {
        this.lado = lado;
    }
    
    public Cuadrado conLado(int nuevoLado) {
        return new Cuadrado(nuevoLado);
    }
}

Sin herencia, sin problema. Cada figura tiene su propio comportamiento coherente.

Opción 2: Interfaz común sin setters

Relación con Diseño por Contratos

LSP está íntimamente relacionado con el Diseño por Contratos (ver Diseño por Contratos):

ConceptoLSPDbC
EntradaPrecondiciones no fortalecidasPrecondiciones del contrato (lo que el cliente debe garantizar)
SalidaPostcondiciones no debilitadasPostcondiciones del contrato (lo que el método garantiza)
EstadoInvariantes preservadosInvariantes de clase (lo que siempre debe ser verdad)

I: Principio de Segregación de Interfaces

Definición

Es preferible tener muchas interfaces pequeñas y específicas que una interfaz grande y general.

Ejemplo: Interfaz Gorda

Considerá una interfaz para dispositivos multifunción:

public interface DispositivoMultifuncion {
    void imprimir(Documento doc);
    void escanear(Documento doc);
    void enviarFax(Documento doc);
    void fotocopiar(Documento doc);
}

Ahora, ¿qué pasa con una impresora simple que solo imprime?

public class ImpresoraBasica implements DispositivoMultifuncion {
    
    @Override
    public void imprimir(Documento doc) {
        // Implementación real
    }
    
    @Override
    public void escanear(Documento doc) {
        throw new UnsupportedOperationException("No puedo escanear");
    }
    
    @Override
    public void enviarFax(Documento doc) {
        throw new UnsupportedOperationException("No puedo enviar fax");
    }
    
    @Override
    public void fotocopiar(Documento doc) {
        throw new UnsupportedOperationException("No puedo fotocopiar");
    }
}

Refactorización: Interfaces Segregadas

public interface Impresora {
    void imprimir(Documento doc);
}

public interface Escaner {
    void escanear(Documento doc);
}

public interface Fax {
    void enviarFax(Documento doc);
}

public interface Fotocopiadora {
    void fotocopiar(Documento doc);
}

// Impresora simple: solo implementa lo que necesita
public class ImpresoraBasica implements Impresora {
    @Override
    public void imprimir(Documento doc) {
        // Implementación real
    }
}

// Dispositivo multifunción: implementa varias interfaces
public class CanonMultifuncion implements Impresora, Escaner, Fax, Fotocopiadora {
    @Override
    public void imprimir(Documento doc) { ... }
    
    @Override
    public void escanear(Documento doc) { ... }
    
    @Override
    public void enviarFax(Documento doc) { ... }
    
    @Override
    public void fotocopiar(Documento doc) { ... }
}

// Scanner de escritorio
public class EscanerEpson implements Escaner {
    @Override
    public void escanear(Documento doc) { ... }
}

Beneficios de ISP

  1. Bajo acoplamiento: Los clientes solo dependen de lo que usan

  2. Compilación selectiva: Cambios en Fax no afectan a ImpresoraBasica

  3. Diseño más claro: Interfaces pequeñas son más fáciles de entender

  4. Composición flexible: Se pueden combinar interfaces según necesidad

ISP y Cohesión de Interfaces

Una buena heurística es que cada interfaz debe representar un rol coherente:

// MAL: Mezcla roles diferentes
interface Usuario {
    void login();
    void logout();
    void cambiarPassword();
    void generarReporte();      // ¿Todos los usuarios generan reportes?
    void administrarUsuarios(); // ¿Todos los usuarios administran?
}

// BIEN: Roles separados
interface Autenticable {
    void login();
    void logout();
    void cambiarPassword();
}

interface GeneradorReportes {
    void generarReporte();
}

interface AdministradorUsuarios {
    void administrarUsuarios();
}

D: Principio de Inversión de Dependencias

Definición

Este principio habla de la dirección de las dependencias en la arquitectura del software.

Ejemplo: Violación de DIP

Considerá un sistema de notificaciones:

// Módulo de bajo nivel (detalle de implementación)
public class EnviadorEmail {
    public void enviar(String destinatario, String mensaje) {
        // Lógica SMTP...
    }
}

// Módulo de alto nivel (lógica de negocio)
public class ServicioNotificaciones {
    private EnviadorEmail enviador = new EnviadorEmail();  // ¡Dependencia directa!
    
    public void notificarUsuario(Usuario usuario, String mensaje) {
        enviador.enviar(usuario.getEmail(), mensaje);
    }
}

Problemas: No se puede cambiar a SMS, difícil de testear, acoplamiento alto.

Refactorización: Invertir la Dependencia

// Abstracción (definida en el nivel de la lógica de negocio)
public interface Notificador {
    void enviar(String destinatario, String mensaje);
}

// Módulo de alto nivel depende de la abstracción
public class ServicioNotificaciones {
    private Notificador notificador;  // Dependencia a abstracción
    
    public ServicioNotificaciones(Notificador notificador) {
        this.notificador = notificador;
    }
    
    public void notificarUsuario(Usuario usuario, String mensaje) {
        notificador.enviar(usuario.getEmail(), mensaje);
    }
}

// Módulos de bajo nivel implementan la abstracción
public class NotificadorEmail implements Notificador {
    @Override
    public void enviar(String destinatario, String mensaje) {
        // Lógica SMTP...
    }
}

public class NotificadorSMS implements Notificador {
    @Override
    public void enviar(String destinatario, String mensaje) {
        // Lógica SMS...
    }
}

public class NotificadorMock implements Notificador {
    @Override
    public void enviar(String destinatario, String mensaje) {
        // Para tests
    }
}

Inversión de Control (IoC)

DIP está relacionado con el concepto de Inversión de Control: en lugar de que el código de alto nivel controle la creación de dependencias, ese control se “invierte” hacia afuera.

Inyección de Dependencias

Es el mecanismo más común para implementar DIP:

// Inyección por constructor (preferido)
public class ServicioNotificaciones {
    private final Notificador notificador;
    
    public ServicioNotificaciones(Notificador notificador) {
        this.notificador = notificador;
    }
}

// Uso
Notificador emailReal = new NotificadorEmail();
ServicioNotificaciones servicio = new ServicioNotificaciones(emailReal);

// Para tests
Notificador mock = new NotificadorMock();
ServicioNotificaciones servicioTest = new ServicioNotificaciones(mock);

¿Quién “Posee” la Abstracción?

Un punto sutil pero importante: la abstracción debe pertenecer al módulo de alto nivel, no al de bajo nivel.


Interrelaciones entre Principios SOLID

Los cinco principios no son independientes; se refuerzan mutuamente:

Ejemplo Integrado: Sistema de Pagos

Veamos cómo todos los principios trabajan juntos:

// ISP: Interfaces segregadas para diferentes capacidades
interface ProcesadorPago {
    ResultadoPago procesar(OrdenPago orden);
}

interface Reembolsable {
    ResultadoReembolso reembolsar(String transaccionId, double monto);
}

interface ConSuscripcion {
    void crearSuscripcion(Usuario usuario, Plan plan);
    void cancelarSuscripcion(String suscripcionId);
}

// LSP: Implementaciones que cumplen contratos
class ProcesadorTarjeta implements ProcesadorPago, Reembolsable {
    @Override
    public ResultadoPago procesar(OrdenPago orden) {
        // Precondición: orden.getMonto() > 0
        // Postcondición: retorna resultado válido (éxito o error con razón)
        // ...
    }
    
    @Override
    public ResultadoReembolso reembolsar(String transaccionId, double monto) {
        // ...
    }
}

class ProcesadorPayPal implements ProcesadorPago, Reembolsable, ConSuscripcion {
    // Implementa todas las interfaces que soporta
}

class ProcesadorTransferencia implements ProcesadorPago {
    // Solo soporta pagos, no reembolsos ni suscripciones
}

// SRP: Cada clase tiene una responsabilidad clara
class ValidadorOrden {
    public void validar(OrdenPago orden) { /* solo validación */ }
}

class RegistradorTransacciones {
    public void registrar(ResultadoPago resultado) { /* solo logging */ }
}

class NotificadorPagos {
    private final Notificador notificador;  // DIP
    
    public void notificar(Usuario usuario, ResultadoPago resultado) {
        // solo notificaciones
    }
}

// OCP + DIP: ServicioPagos cerrado para modificación, abierto para extensión
class ServicioPagos {
    private final ProcesadorPago procesador;  // DIP: depende de abstracción
    private final ValidadorOrden validador;
    private final RegistradorTransacciones registrador;
    private final NotificadorPagos notificador;
    
    // Inyección de dependencias
    public ServicioPagos(
            ProcesadorPago procesador,
            ValidadorOrden validador,
            RegistradorTransacciones registrador,
            NotificadorPagos notificador) {
        this.procesador = procesador;
        this.validador = validador;
        this.registrador = registrador;
        this.notificador = notificador;
    }
    
    public ResultadoPago procesarPago(Usuario usuario, OrdenPago orden) {
        validador.validar(orden);
        ResultadoPago resultado = procesador.procesar(orden);
        registrador.registrar(resultado);
        notificador.notificar(usuario, resultado);
        return resultado;
    }
}

Para agregar un nuevo procesador (ej: criptomonedas):

  1. Crear ProcesadorCripto implements ProcesadorPago

  2. Inyectarlo en ServicioPagos

  3. ¡Sin modificar código existente! ✓


Antipatrones y Errores Comunes

Sobre-Ingeniería por SOLID

Aplicar SOLID ciegamente puede llevar a código innecesariamente complejo:

// ¿Realmente necesitamos todo esto para sumar dos números?

interface Sumador {
    int sumar(int a, int b);
}

interface SumadorFactory {
    Sumador crear();
}

class SumadorImpl implements Sumador {
    @Override
    public int sumar(int a, int b) {
        return a + b;
    }
}

class SumadorFactoryImpl implements SumadorFactory {
    @Override
    public Sumador crear() {
        return new SumadorImpl();
    }
}

// vs

class Matematica {
    static int sumar(int a, int b) {
        return a + b;
    }
}

Principios Mal Aplicados

PrincipioMal AplicadoConsecuencia
SRPUna clase por métodoExplosión de clases
OCPInterfaces para todoAbstracción prematura
LSPEvitar toda herenciaDuplicación de código
ISPInterfaces de un métodoFragmentación excesiva
DIPInyectar todoConfiguración compleja

Encontrar el Balance


Resumen


Ejercicios

Solution to Exercise 1

La clase tiene al menos 5 responsabilidades:

  1. Datos del usuario (nombre, email, password)

  2. Persistencia (guardar en BD)

  3. Comunicación (enviar email)

  4. Autenticación (validar password)

  5. Serialización (JSON import/export)

Refactorización:

// Solo datos
public class Usuario {
    private String nombre;
    private String email;
    private String passwordHash;
    // getters/setters
}

// Persistencia
public class UsuarioRepository {
    public void guardar(Usuario usuario) { ... }
    public Usuario buscar(String email) { ... }
}

// Comunicación
public class NotificadorUsuario {
    public void enviarBienvenida(Usuario usuario) { ... }
}

// Autenticación
public class ServicioAutenticacion {
    public boolean validarCredenciales(Usuario u, String password) { ... }
}

// Serialización
public class UsuarioSerializer {
    public String toJSON(Usuario usuario) { ... }
    public Usuario fromJSON(String json) { ... }
}
Solution to Exercise 2
// Abstracción para descuentos
public interface Descuento {
    double aplicar(double precioBase);
}

// Implementaciones
public class SinDescuento implements Descuento {
    @Override
    public double aplicar(double precioBase) {
        return precioBase;
    }
}

public class DescuentoPorcentual implements Descuento {
    private final double porcentaje;
    
    public DescuentoPorcentual(double porcentaje) {
        this.porcentaje = porcentaje;
    }
    
    @Override
    public double aplicar(double precioBase) {
        return precioBase * (1 - porcentaje / 100);
    }
}

public class DescuentoMontoFijo implements Descuento {
    private final double monto;
    
    public DescuentoMontoFijo(double monto) {
        this.monto = monto;
    }
    
    @Override
    public double aplicar(double precioBase) {
        return Math.max(0, precioBase - monto);
    }
}

// Calculador cerrado para modificación
public class CalculadorPrecio {
    public double calcular(Producto producto, Descuento descuento) {
        return descuento.aplicar(producto.getPrecioBase());
    }
}

// Agregar nuevo descuento: solo crear nueva clase
public class DescuentoBlackFriday implements Descuento {
    @Override
    public double aplicar(double precioBase) {
        return precioBase * 0.5;  // 50% off
    }
}
Solution to Exercise 3

Violación de LSP:

El código cliente que trabaja con Ave espera poder llamar a volar() sin excepciones. Pinguino rompe esta expectativa lanzando una excepción.

void hacerVolarAves(List<Ave> aves) {
    for (Ave ave : aves) {
        ave.volar();  // ¡Explota con Pinguino!
    }
}

Solución: Segregar la capacidad de volar

public interface Ave {
    void comer();
    void moverse();
}

public interface Volador {
    void volar();
}

public class Paloma implements Ave, Volador {
    @Override
    public void comer() { ... }
    
    @Override
    public void moverse() {
        volar();  // Las palomas se mueven volando
    }
    
    @Override
    public void volar() {
        System.out.println("Volando...");
    }
}

public class Pinguino implements Ave {
    @Override
    public void comer() { ... }
    
    @Override
    public void moverse() {
        System.out.println("Caminando/nadando...");
    }
    // No implementa Volador → no tiene volar()
}

// Código cliente correcto
void hacerVolarVoladores(List<Volador> voladores) {
    for (Volador v : voladores) {
        v.volar();  // Solo acepta cosas que vuelan
    }
}
Solution to Exercise 4
// Actividades básicas de cualquier trabajador
public interface Trabajador {
    void trabajar();
}

// Necesidades humanas (no aplica a robots)
public interface SerHumano {
    void comer();
    void dormir();
}

// Gestión de tiempo
public interface EmpleadoConHorario {
    void reportarHoras();
    void solicitarVacaciones();
}

// Aspectos salariales
public interface Asalariado {
    void calcularSalario();
}

// Capacidades de liderazgo
public interface Gerente {
    void administrarEquipo();
}

// Capacidades de RRHH
public interface RecursosHumanos {
    void contratarPersonal();
    void despedirPersonal();
}

// Implementaciones
public class EmpleadoComun implements Trabajador, SerHumano, 
                                      EmpleadoConHorario, Asalariado {
    // Implementa lo que necesita
}

public class GerenteArea implements Trabajador, SerHumano, 
                                    EmpleadoConHorario, Asalariado, 
                                    Gerente {
    // Puede administrar equipo
}

public class DirectorRRHH implements Trabajador, SerHumano,
                                     EmpleadoConHorario, Asalariado,
                                     RecursosHumanos {
    // Puede contratar/despedir
}

public class RobotTrabajador implements Trabajador {
    // Solo trabaja, no come ni duerme
}
Solution to Exercise 5
// Abstracciones (definidas en el módulo de negocio)
public interface FuenteDatos {
    String obtenerDatos(String consulta);
}

public interface GeneradorDocumento {
    byte[] generar(String contenido);
}

public interface EnviadorMensajes {
    void enviar(String destinatario, String asunto, byte[] adjunto);
}

// Implementaciones (módulos de bajo nivel)
public class MySQLFuenteDatos implements FuenteDatos {
    @Override
    public String obtenerDatos(String consulta) {
        // Lógica MySQL
    }
}

public class GeneradorPDF implements GeneradorDocumento {
    @Override
    public byte[] generar(String contenido) {
        // Lógica PDF
    }
}

public class EnviadorSMTP implements EnviadorMensajes {
    @Override
    public void enviar(String destinatario, String asunto, byte[] adjunto) {
        // Lógica SMTP
    }
}

// Módulo de alto nivel con dependencias inyectadas
public class GeneradorReportes {
    private final FuenteDatos fuenteDatos;
    private final GeneradorDocumento generadorDoc;
    private final EnviadorMensajes enviador;
    
    public GeneradorReportes(
            FuenteDatos fuenteDatos,
            GeneradorDocumento generadorDoc,
            EnviadorMensajes enviador) {
        this.fuenteDatos = fuenteDatos;
        this.generadorDoc = generadorDoc;
        this.enviador = enviador;
    }
    
    public void generarYEnviarReporte(String email, String consulta) {
        String datos = fuenteDatos.obtenerDatos(consulta);
        byte[] documento = generadorDoc.generar(datos);
        enviador.enviar(email, "Reporte", documento);
    }
}

// Configuración (composición root)
FuenteDatos db = new MySQLFuenteDatos();
GeneradorDocumento gen = new GeneradorPDF();
EnviadorMensajes mail = new EnviadorSMTP();

GeneradorReportes reportes = new GeneradorReportes(db, gen, mail);
reportes.generarYEnviarReporte("usuario@email.com", "SELECT * FROM ventas");

// Para tests
GeneradorReportes reportesTest = new GeneradorReportes(
    mockFuenteDatos,
    mockGenerador,
    mockEnviador
);

Lecturas Recomendadas

Próximo paso

Para seguir, conviene pasar a el material siguiente, donde el recorrido continúa sobre esta base.