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:
Rigidez: Cambios pequeños requieren modificar muchos archivos
Fragilidad: Arreglar un bug introduce nuevos bugs
Inmovilidad: Es imposible reutilizar código en otros contextos
Viscosidad: Es más fácil hacer las cosas mal que bien
SOLID como Antídoto¶
Los principios SOLID atacan directamente estos síntomas:
| Principio | Combate |
|---|---|
| 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:
Contabilidad podría cambiar la fórmula de cálculo salarial
RRHH podría modificar el formato del reporte
IT podría cambiar el esquema de base de datos
Problemas de Esta Violación¶
Acoplamiento no deseado: Un cambio pedido por Contabilidad podría afectar a RRHH o IT
Compilación innecesaria: Modificar el reporte obliga a recompilar y redesplegar todo
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 nada2. Patrón Strategy¶
interface EstrategiaDescuento {
double aplicar(double precio);
}
class SinDescuento implements EstrategiaDescuento { ... }
class DescuentoPorcentual implements EstrategiaDescuento { ... }
class DescuentoBlackFriday implements EstrategiaDescuento { ... } // Nuevo3. 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):
| Concepto | LSP | DbC |
|---|---|---|
| Entrada | Precondiciones no fortalecidas | Precondiciones del contrato (lo que el cliente debe garantizar) |
| Salida | Postcondiciones no debilitadas | Postcondiciones del contrato (lo que el método garantiza) |
| Estado | Invariantes preservados | Invariantes 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¶
Bajo acoplamiento: Los clientes solo dependen de lo que usan
Compilación selectiva: Cambios en
Faxno afectan aImpresoraBasicaDiseño más claro: Interfaces pequeñas son más fáciles de entender
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):
Crear
ProcesadorCripto implements ProcesadorPago✓Inyectarlo en
ServicioPagos✓¡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¶
| Principio | Mal Aplicado | Consecuencia |
|---|---|---|
| SRP | Una clase por método | Explosión de clases |
| OCP | Interfaces para todo | Abstracción prematura |
| LSP | Evitar toda herencia | Duplicación de código |
| ISP | Interfaces de un método | Fragmentación excesiva |
| DIP | Inyectar todo | Configuración compleja |
Encontrar el Balance¶
Resumen¶
Ejercicios¶
Solution to Exercise 1
La clase tiene al menos 5 responsabilidades:
Datos del usuario (nombre, email, password)
Persistencia (guardar en BD)
Comunicación (enviar email)
Autenticación (validar password)
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¶
Martin, R. C. (2003). Agile Software Development: Principles, Patterns, and Practices
Martin, R. C. (2017). Clean Architecture
Meyer, B. (1997). Object-Oriented Software Construction (2nd ed.)
Fowler, M. (2018). Refactoring: Improving the Design of Existing Code (2nd ed.)
Próximo paso¶
Para seguir, conviene pasar a el material siguiente, donde el recorrido continúa sobre esta base.