La lección anterior terminó con un diagnóstico: el dominio de BiblioTech ya no se puede corromper desde fuera, pero la superficie pública de sus clases ha crecido sin control. Prestamo expone más de veinte métodos, entre ellos un getEmpleadoNoUsar() cuyo propio nombre confiesa que no debería existir. Encapsular responde a la pregunta cómo oculto lo que hay dentro; queda por responder la más difícil: qué merece estar fuera. Ese es el territorio de la abstracción, el cuarto pilar de la POO y, a diferencia de los otros tres, no un mecanismo del lenguaje sino una disciplina de diseño: quedarse con lo esencial para el problema que se resuelve y descartar todo lo demás. Un mapa de metro es una abstracción magistral —miente sobre las distancias, ignora la geografía y sin embargo te lleva a tu destino mejor que un plano a escala—. Esta lección te enseña a diseñar así: a decidir qué expones, a no mezclar niveles dentro de un método, a documentar contratos y a reconocer tanto las abstracciones que gotean como el vicio contrario, abstraer lo que nadie ha pedido.

Contenido

  1. La abstracción como disciplina de diseño
  2. Abstracción frente a encapsulamiento
  3. Niveles de abstracción y por qué no deben mezclarse
  4. Refactorización: separar la regla de la presentación
  5. Diseño por contrato: precondiciones, postcondiciones e invariantes
  6. El vocabulario del dominio
  7. Separar el qué del cómo
  8. API pública mínima y el coste de exponer de más
  9. Abstracciones con fugas
  10. Sobreabstracción y YAGNI
  11. Los mecanismos que faltan: interfaces y clases abstractas
  12. Errores Comunes y Consejos
  13. Ejercicios

  1. La abstracción como disciplina de diseño

Abstraer es quedarse con lo esencial y descartar lo accidental. La palabra clave es "para el problema": no existe la abstracción correcta en absoluto, solo la correcta para un propósito.

El mismo libro físico se abstrae de forma distinta según quién lo modele:

Sistema Qué es esencial Qué se descarta
BiblioTech (préstamos) Título, ISBN, disponibilidad, plazo Peso, número de páginas, color de portada
Tienda online Precio, existencias, imagen de portada, envío Disponibilidad de préstamo
Imprenta Gramaje del papel, tinta, formato, encuadernación ISBN, autor
Servicio de mudanzas Peso y volumen Absolutamente todo lo demás

Ninguna es más "verdadera" que otra. Un Libro en BiblioTech no tiene peso porque a BiblioTech no le importa el peso, y añadirlo "por si acaso" sería contaminar el modelo.

De ahí la primera pregunta que debes hacerte ante cualquier clase:

¿Qué necesita saber y hacer este objeto para el problema que resolvemos? Todo lo demás sobra.

  1. Abstracción frente a encapsulamiento

Los dos conceptos se confunden constantemente porque colaboran, pero responden a preguntas distintas:

Abstracción Encapsulamiento
Pregunta que responde ¿Qué expongo? ¿Cómo lo oculto?
Naturaleza Decisión de diseño Mecanismo del lenguaje
Momento Al modelar, antes de escribir código Al implementar
Herramientas Elegir clases, operaciones y nombres private, protected, getters, operaciones de negocio
Se equivoca cuando... Expones lo que no importa u ocultas lo que sí Dejas puertas abiertas al estado interno
En BiblioTech Decidir que Prestamo ofrece registrarDevolucion(int) Hacer private el campo multaAplicada
Analogía del coche El volante y los pedales son la abstracción de "conducir" El capó cerrado impide tocar el motor

Se puede tener una sin la otra, y ambos fallos son reales:

  • Buen encapsulamiento, mala abstracción: todos los campos private, pero con treinta getters y setters validados. Nada se corrompe, pero la clase no significa nada; es una hoja de cálculo con sintaxis Java.
  • Buena abstracción, mal encapsulamiento: una API pública magnífica —prestar(), devolver(), calcularMulta()— junto a campos public que permiten saltársela. El diseño es correcto y no se cumple.

Necesitas las dos. La abstracción decide qué puertas hay; el encapsulamiento garantiza que no hay otras.

  1. Niveles de abstracción y por qué no deben mezclarse

Un programa tiene capas de detalle, del concepto al bit:

flowchart TD
    A["Nivel 4: caso de uso
    'registrar la devolucion de un libro'"] --> B["Nivel 3: reglas de negocio
    calcular retraso, aplicar tope, clasificar"]
    B --> C["Nivel 2: operaciones de dominio
    material.devolver(), empleado.registrarDevolucion()"]
    C --> D["Nivel 1: mecanica del lenguaje
    Math.min, printf, bucles, aritmetica"]

La regla, conocida como principio de nivel único de abstracción:

Dentro de un mismo método, todas las sentencias deberían pertenecer aproximadamente al mismo nivel.

¿Por qué? Porque leer un método que salta de nivel obliga a cambiar de marco mental en cada línea. Un método que dice "registra la devolución, y por cierto aquí va la fórmula del IVA y el ancho de columna del recibo" no se puede leer en diagonal: hay que leerlo entero, cada vez.

Este es el aspecto de la mezcla, y probablemente reconozcas el estilo porque es exactamente el de tu main del módulo 2:

// MAL: cuatro niveles de abstraccion en catorce lineas
public void procesarDevolucion(Prestamo prestamo, int dias) {

    System.out.println("========================================");     // nivel 1
    System.out.println("   RECIBO DE DEVOLUCION - BIBLIOTECH");         // nivel 1

    int retraso = Math.max(0, dias - prestamo.getMaterial().getDiasPrestamo());  // nivel 3
    double multa = retraso * prestamo.getMaterial().getTarifaDiaria();           // nivel 3
    if (multa > 20.0) {                                                          // nivel 3
        multa = 20.0;
    }

    System.out.printf("  %-20s %s%n", "Material:", prestamo.getTituloMaterial()); // nivel 1
    System.out.printf("  %-20s %d dias%n", "Retraso:", retraso);                  // nivel 1
    System.out.printf("  %-20s %.2f EUR%n", "Multa:", multa);                     // nivel 1

    prestamo.getMaterial().devolver();                                            // nivel 2
    System.out.println("========================================");               // nivel 1
}

Los problemas concretos, más allá de la estética:

  1. No se puede reutilizar la regla. Si otro punto del programa necesita la multa, tiene que copiar la fórmula o llamar a este método y aguantar el recibo por consola.
  2. No se puede probar. Para verificar el cálculo hay que capturar la salida estándar (módulo 11).
  3. No se puede cambiar el canal. Si mañana el recibo es una página web o un PDF, hay que reescribir la lógica de negocio junto con el formato.
  4. El tope de 20 € está duplicado. Ya vive en Material.MULTA_MAXIMA, y aquí aparece como literal. Dos verdades para un mismo hecho.
  5. Viola la Ley de Demeter tres veces con prestamo.getMaterial().getX().

  1. Refactorización: separar la regla de la presentación

Reescribamos el método anterior repartiendo cada línea a su nivel.

Nivel 3, reglas de negocio: ya están en el dominio. Prestamo.registrarDevolucion(int) calcula la multa, marca el préstamo, libera el material y descuenta al empleado (lección 03-07). No hay que escribir nada nuevo.

Nivel 1, presentación: una clase aparte, fuera del dominio.

package com.nexussoftware.bibliotech.presentacion;

import com.nexussoftware.bibliotech.dominio.Prestamo;

/** Genera los textos que BiblioTech muestra por consola. */
public class ReciboConsola {

    private static final String SEPARADOR = "========================================";

    /** Imprime el recibo de una devolucion ya registrada. */
    public void imprimirRecibo(Prestamo prestamo) {
        System.out.println(SEPARADOR);
        System.out.println("   RECIBO DE DEVOLUCION - BIBLIOTECH");
        System.out.printf("  %-20s %s%n",       "Referencia:", prestamo.getReferencia());
        System.out.printf("  %-20s %s%n",       "Material:",   prestamo.getTituloMaterial());
        System.out.printf("  %-20s %s%n",       "Empleado:",   prestamo.getNombreEmpleado());
        System.out.printf("  %-20s %d dias%n",  "Retraso:",    prestamo.calcularDiasRetraso());
        System.out.printf("  %-20s %.2f EUR%n", "Multa:",      prestamo.getMultaAplicada());
        System.out.printf("  %-20s %s%n",       "Gravedad:",   prestamo.clasificarGravedad());
        System.out.println(SEPARADOR);
    }
}

Nivel 4, el caso de uso: coordina, no calcula ni imprime.

/** Registra la devolucion de un prestamo y entrega el recibo al empleado. */
public void procesarDevolucion(Prestamo prestamo, int diasTranscurridos) {
    prestamo.registrarDevolucion(diasTranscurridos);
    recibo.imprimirRecibo(prestamo);
}

Dos líneas. Y cada una a su nivel.

Compara el antes y el después:

Criterio Antes Después
Niveles mezclados en un método 4 1
Dónde está la fórmula de la multa Duplicada en el método Solo en Material
El literal 20.0 Escrito a mano MULTA_MAXIMA
Cambiar a salida web Reescribir el método Otra clase de presentación
Probar el cálculo Capturando System.out Llamando a calcularMulta()
Violaciones de Demeter 3 0
Líneas en el caso de uso 14 2

Nota el paquete nuevo, presentacion. La estructura de BiblioTech empieza a reflejar sus niveles de abstracción:

com.nexussoftware.bibliotech
├── BiblioTechApp.java          arranque
├── dominio/                    QUE es el negocio y sus reglas
├── presentacion/               COMO se muestra
└── servicio/                   (modulo 5) casos de uso

Esta separación es la semilla de la arquitectura por capas que verás completa en el módulo 12.

  1. Diseño por contrato: precondiciones, postcondiciones e invariantes

Una abstracción es una promesa: "llámame así y te devolveré esto". Cuanto más explícita sea la promesa, menos margen hay para malentendidos. El diseño por contrato formaliza esa promesa en tres partes:

Elemento Qué es Quién lo garantiza
Precondición Lo que debe ser cierto antes de llamar El llamador
Postcondición Lo que será cierto después de la llamada El método
Invariante Lo que es cierto siempre, antes y después La clase

En Java no hay sintaxis para contratos (otros lenguajes sí la tienen), así que se documentan con Javadoc, que ya conoces del módulo 1:

/**
 * Registra la devolucion del material prestado y aplica la multa que corresponda.
 *
 * <p><b>Precondiciones:</b>
 * <ul>
 *   <li>{@code diasTranscurridos >= 0}.</li>
 *   <li>El prestamo no debe estar ya devuelto; si lo esta, la llamada no
 *       tiene efecto y se devuelve la multa aplicada anteriormente.</li>
 * </ul>
 *
 * <p><b>Postcondiciones:</b>
 * <ul>
 *   <li>{@code estaDevuelto()} pasa a ser {@code true}.</li>
 *   <li>El material vuelve a estar disponible.</li>
 *   <li>El empleado tiene un prestamo activo menos.</li>
 *   <li>El valor devuelto esta entre 0 y {@code Material.MULTA_MAXIMA}.</li>
 * </ul>
 *
 * <p><b>Invariante de clase mantenida:</b> {@code diasTranscurridos >= 0} y
 * {@code diaVencimiento == diaPrestamo + material.getDiasPrestamo()}.
 *
 * @param diasTranscurridos dias transcurridos desde la entrega, nunca negativo
 * @return importe de la multa aplicada, en euros
 */
public double registrarDevolucion(int diasTranscurridos) { ... }

Ventajas de escribirlo:

  • El llamador sabe qué debe cumplir sin leer la implementación. Eso es, literalmente, la definición de abstracción.
  • Te obliga a ti, al escribirlo, a pensar en los casos límite: ¿qué pasa si ya estaba devuelto? ¿Y si los días son negativos? Muchos contratos revelan huecos de diseño antes de que existan como bugs.
  • El IDE lo muestra al autocompletar, así que el contrato viaja con el código.

Y una guía práctica sobre precondiciones: hay dos estrategias, y conviene elegir conscientemente.

Estrategia Cómo Cuándo
Defensiva Comprobar y rechazar (avisos ahora, excepciones en el módulo 6) API pública, entrada del usuario
Por contrato estricto Documentar la precondición y asumir que se cumple Métodos private, código interno

Comprobarlo todo en todas partes multiplica el código sin aportar seguridad: si un método private solo lo llaman otros dos métodos de la misma clase que ya validaron, volver a validar es ruido.

  1. El vocabulario del dominio

Una buena abstracción habla el idioma del negocio. Si un bibliotecario de Nexus Software leyera tu código por encima del hombro, debería reconocer las palabras.

Compara:

// Vocabulario tecnico: no significa nada para el negocio
DataManager dm = new DataManager();
dm.process(item, 20);
if (dm.getStatus() == 2) { ... }

// Vocabulario del dominio
double multa = prestamo.registrarDevolucion(20);
if (prestamo.estaVencido()) { ... }

A la idea de usar un único vocabulario compartido entre el código, la documentación y las conversaciones con el negocio se le llama lenguaje ubicuo. Su regla práctica es simple: si el negocio dice "multa", el código dice multa, no penaltyAmount, fee ni charge.

El vocabulario de BiblioTech, que llevas usando desde el módulo 1, es exactamente esto: titulo, autor, isbn, disponible, diasRetraso, multa, gravedad, referencia, prestar, devolver. Ninguna palabra técnica se ha colado en el dominio, y por eso prestamo.registrarDevolucion(20) se entiende sin abrir la clase.

  1. Separar el qué del cómo

El resultado tangible de una buena abstracción es que el llamador deja de saber cómo se hacen las cosas.

Recorre lo que ha pasado con el cálculo de la multa a lo largo del curso:

Momento Quién sabe cómo se calcula
Módulo 1 main: la fórmula está escrita ahí
Módulo 2 main: la fórmula, el tope y la clasificación, dentro de un switch
Lección 03-03 Prestamo: main llama a calcularMulta()
Lección 03-05 Material y sus subclases: cada soporte aporta su tarifa
Lección 03-07 Solo el dominio: main ni siquiera puede tocar el resultado
Ahora main ni siquiera menciona la palabra "multa" salvo para mostrarla

La prueba definitiva de que el qué y el cómo están separados es esta: ¿puedes cambiar la implementación sin tocar a los llamadores? Si Nexus Software decide mañana que la tarifa sea progresiva —0,25 € los siete primeros días y 0,50 € a partir del octavo—, ¿cuántos ficheros hay que tocar?

// Nueva regla, solo en Material
@Override
public double calcularMulta(int diasTranscurridos) {
    int retraso = calcularDiasRetraso(diasTranscurridos);
    int tramoLeve  = Math.min(retraso, UMBRAL_LEVE);
    int tramoGrave = Math.max(0, retraso - UMBRAL_LEVE);
    double importe = tramoLeve * getTarifaDiaria()
                   + tramoGrave * getTarifaDiaria() * 2;
    return Math.min(importe, MULTA_MAXIMA);
}

Un fichero. Ni main, ni el recibo, ni Prestamo se enteran. Eso es lo que compra la abstracción, y es la razón por la que se paga su coste en indirección.

  1. API pública mínima y el coste de exponer de más

La API pública de una clase es todo lo que otros pueden usar: métodos y campos public, y sus constructores. Es su promesa hacia el exterior.

Y esa promesa tiene un coste que casi nadie calcula al escribir el código:

Coste Explicación
Compatibilidad Cada método público es un compromiso: si lo borras o cambias su firma, rompes a todos sus usuarios
Carga cognitiva Una clase con 25 métodos públicos obliga a leer 25 nombres para saber cuál usar
Superficie de error Cada método público es una vía por la que alguien puede usar mal la clase
Libertad perdida No puedes cambiar lo que has prometido; solo lo privado es libre

La regla de oro:

Es fácil añadir un método público más tarde; es carísimo quitarlo. Ante la duda, déjalo private.

Aplicado a Prestamo, revisemos su API actual con un criterio severo:

Miembro ¿Público? Razón
registrarDevolucion(int) Es el caso de uso
calcularMulta(), calcularDiasRetraso(), clasificarGravedad() Consultas que necesita la presentación
getReferencia(), getTituloMaterial(), getNombreEmpleado() Identifican el préstamo en pantalla
estaDevuelto(), estaVencido(), getDiaVencimiento() Estado consultado en listados
getMultaAplicada() Hecho registrado tras la devolución
setDiasTranscurridos(int) Noprivate Es un detalle interno de registrarDevolucion
getEmpleadoNoUsar() No → eliminar Permite mutar el empleado por la puerta de atrás
getMaterial() Discutible Necesario en algunos listados; se puede sustituir por delegaciones
getDiaPrestamo(), diasRestantes() Se muestran en el resumen
empleadoEnElLimite() Consulta de negocio legítima
sePuedePrestar(Material, Empleado) Comprobación previa al caso de uso

Con esa poda, Prestamo pasa de más de veinte miembros públicos a catorce, y ninguno permite descuadrar el sistema. El ejercicio 1 te pide llevarla hasta el final.

  1. Abstracciones con fugas

Una abstracción con fugas (leaky abstraction) es la que promete ocultar un detalle pero obliga a conocerlo igualmente. La ley enunciada por Joel Spolsky dice, con algo de humor negro, que todas las abstracciones no triviales gotean: la cuestión no es evitarlo del todo, sino no agravarlo.

Ejemplos que ya has visto o verás:

Fuga Cómo se nota
Prestamo.getMaterial() devuelve el objeto mutable Quien lo recibe puede llamar a devolver() saltándose el préstamo
Un getIncidencias() sin copia defensiva El llamador descubre que su array es el interno
Un método llamado guardar() que a veces falla por red La abstracción "guardar" no puede ocultar que hay una red debajo (módulo 9)
String.substring y el uso de memoria En versiones antiguas de Java, una subcadena retenía todo el array original

Cómo reducir las fugas:

  • Devuelve tipos inmutables o copias en lugar de estado interno.
  • No expongas tipos de la implementación en la firma pública. Si mañana quieres cambiar un array por una lista, tu firma no debería obligarte a romper a nadie.
  • Documenta lo que gotea cuando no puedas evitarlo. Una fuga documentada es un contrato; una fuga silenciosa es una trampa.

  1. Sobreabstracción y YAGNI

El vicio opuesto también existe, y en manos de alguien que acaba de descubrir la POO es el más frecuente: abstraer lo que nadie ha pedido.

Síntomas de sobreabstracción:

  • Jerarquías de cinco niveles para tres casos concretos.
  • Clases llamadas AbstractBaseGenericProcessorFactory.
  • Parámetros de configuración que siempre valen lo mismo.
  • Puntos de extensión "por si algún día" que nunca se usan.
  • Una interfaz por cada clase, con una única implementación.

El principio que lo contiene se llama YAGNI: You Aren't Gonna Need It, "no lo vas a necesitar". Formulado como regla:

No añadas flexibilidad hasta que tengas dos casos reales que la requieran. Con uno, es adivinación.

Aplicado a BiblioTech, un contraste honesto:

Abstracción ¿Justificada? Por qué
Material con tres subclases Hay tres soportes reales con reglas distintas
Separar dominio de presentacion El módulo 12 traerá una interfaz web
getDiasPrestamo() sobrescribible Cada soporte tiene su plazo, comprobado
Una jerarquía MaterialFisico / MaterialDigital No, todavía No hay ningún material digital en el catálogo
Un sistema de tarifas configurable desde fichero No Hay una tarifa por soporte y no cambia
Interfaz Prestable con un solo implementador No No aporta nada hasta que haya un segundo

Y una advertencia sobre el equilibrio: sobreabstraer es un error caro pero visible; infraabstraer produce el main de doscientas líneas del módulo 2, que también lo es. La respuesta profesional no es elegir un extremo, sino refactorizar cuando aparece el segundo caso. Es exactamente lo que has hecho: Material no existía hasta que aparecieron revistas y DVD.

  1. Los mecanismos que faltan: interfaces y clases abstractas

Toda esta lección ha hablado de abstracción como disciplina: qué modelar, qué exponer, cómo separar niveles. Pero Java ofrece además dos mecanismos del lenguaje diseñados específicamente para materializarla, y ambos llegan en el módulo siguiente:

Mecanismo Qué aporta Lección
Interfaces Declaran qué se puede hacer sin decir cómo; una clase puede implementar varias 04-01
Clases abstractas Clases que no se pueden instanciar y que pueden declarar métodos sin cuerpo, obligando a las subclases a implementarlos 04-02

Dos carencias concretas del diseño actual que esos mecanismos resolverán:

Primera: Material se puede instanciar. Hoy nada impide escribir new Material("Algo", "REF-1", true), y eso no significa nada: en la biblioteca hay libros, revistas y DVD, no "materiales genéricos". Una clase abstracta lo prohíbe a nivel de compilador.

Segunda: getTipo() devuelve "Material" por defecto. Si alguien crea una subclase nueva y olvida sobrescribirlo, aparecerá "Material" en los listados sin que nadie lo detecte. Un método abstracto convierte ese olvido en un error de compilación:

// Adelanto de la leccion 04-02
public abstract class Material {
    public abstract String getTipo();     // sin cuerpo: obliga a implementarlo
}

Quédate con esto: en el módulo 4 aprenderás cómo se escriben; en esta lección has aprendido por qué existen. El orden importa, porque quien aprende interface sin haber entendido la abstracción acaba creando una interfaz por clase sin saber para qué.

Errores Comunes y Consejos

  • Confundir abstracción con encapsulamiento. Abstracción es qué expongo; encapsulamiento es cómo lo oculto. La tabla del apartado 2 los separa.
  • Mezclar niveles en un método. Reglas de negocio y printf en el mismo bloque es el error más frecuente en código de principiante, y el más caro de deshacer.
  • Meter presentación en las clases de dominio. Un Libro que sabe imprimir su ficha por consola no podrá reutilizarse en una web. La presentación va en otro paquete.
  • Hacer público "por si acaso". Cada método público es un compromiso permanente. Empieza private y sube solo cuando alguien lo necesite de verdad.
  • Nombres genéricos. Manager, Processor, Helper, Data, Info, Util. Si no puedes ponerle un nombre concreto a una clase, probablemente no sabes aún qué es.
  • Documentar el cómo en lugar del qué. Un Javadoc que dice "recorre el array y suma" se queda obsoleto en cuanto cambies la implementación. Documenta el contrato: qué recibe, qué devuelve, qué garantiza.
  • Abstraer sin un segundo caso. YAGNI. Espera a que aparezca el segundo soporte, la segunda base de datos, el segundo formato.
  • Consejo: describe el método en una frase. Si necesitas "y" o "además", el método hace dos cosas y probablemente mezcla niveles.
  • Consejo: lee tu código como si fueras el bibliotecario. Si p.registrarDevolucion(20) se entiende y dm.process(i, 20) no, ya sabes cuál está bien nombrado.
  • Consejo: la prueba del cambio. Ante cada decisión de diseño, pregúntate qué habría que tocar si la regla cambiara. Cuantos menos ficheros, mejor la abstracción.

Ejercicios

Ejercicio 1: rediseñar la API pública de Prestamo

Este es el ejercicio central de la lección. Toma la clase Prestamo tal como quedó en 03-07 y reduce su superficie pública al mínimo necesario.

  1. Lista todos sus miembros públicos actuales.
  2. Para cada uno, decide: mantener, hacer private o eliminar, justificando la decisión en una frase.
  3. Elimina getEmpleadoNoUsar() y sustituye sus usos por delegaciones adecuadas.
  4. Escribe el Javadoc completo, con precondiciones y postcondiciones, de los tres métodos que consideres el núcleo de la clase.
  5. Comprueba que BiblioTechApp y ReciboConsola siguen funcionando con la API reducida.

Ejercicio 2: separar niveles de abstracción

Refactoriza este método repartiendo cada línea a su nivel. Debe quedar: las reglas en el dominio, la presentación en ReciboConsola y el caso de uso reducido a tres o cuatro líneas legibles.

public static void procesarPrestamoCompleto(Material m, Empleado e, int dia, Scanner sc) {
    System.out.println("--- NUEVO PRESTAMO ---");
    if (m.estaDisponible() == false) {
        System.out.println("ERROR: no disponible");
        return;
    }
    if (e.getPrestamosAcumulados() >= 3) {
        System.out.println("ERROR: limite alcanzado");
        return;
    }
    System.out.print("Confirmar (si/no): ");
    String r = sc.nextLine().trim();
    if (!r.equalsIgnoreCase("si")) {
        System.out.println("Cancelado");
        return;
    }
    Prestamo p = new Prestamo(m, e, dia);
    int vence = dia + m.getDiasPrestamo();
    System.out.println("OK. Vence el dia " + vence
                       + ". Tarifa de retraso: " + m.getTarifaDiaria() + " EUR/dia"
                       + ". Tope: " + 20.0 + " EUR");
}

Ejercicio 3: juzgar abstracciones

Para cada propuesta de un compañero, decide si está justificada, si es sobreabstracción (YAGNI) o si es una abstracción con fugas. Justifica en dos o tres frases y, cuando proceda, propón la alternativa.

  1. Crear MaterialFisico y MaterialDigital como niveles intermedios entre Material y las tres clases actuales.
  2. Añadir a Material un método public String[] getCamposInternos() que devuelva el estado en bruto "para depurar".
  3. Extraer las cuatro constantes de negocio a una clase ReglasNegocio con métodos static.
  4. Añadir a Prestamo un parámetro boolean modoSilencioso en registrarDevolucion para no imprimir avisos.

Soluciones

Solución 1

1 y 2. Revisión miembro por miembro:

Miembro Decisión Justificación
Prestamo(Material, Empleado, int, int) Mantener Constructor canónico
Prestamo(Material, Empleado, int) Mantener Alta habitual, con cero días
getReferencia() Mantener Identifica el préstamo en pantalla y en avisos
getTituloMaterial() Mantener Delegación conforme a Demeter
getNombreEmpleado() Mantener Delegación conforme a Demeter
getMaterial() Eliminar Expone un colaborador mutable; se sustituye por delegaciones
getEmpleadoNoUsar() Eliminar Permite mutar el empleado saltándose el préstamo
getDiaPrestamo() Mantener Se muestra en el resumen
getDiaVencimiento() Mantener Dato clave para el usuario
getDiasTranscurridos() Mantener Se muestra y se consulta
setDiasTranscurridos(int) A private Detalle interno de registrarDevolucion
estaDevuelto() Mantener Estado consultado en listados
estaVencido() Mantener Consulta de negocio
diasRestantes() Mantener Se muestra en avisos de vencimiento
calcularDiasRetraso() Mantener La presentación lo necesita
calcularMulta() Mantener Consulta previa a la devolución
clasificarGravedad() Mantener Se muestra en el recibo
getMultaAplicada() Mantener Hecho registrado tras devolver
getIncidencias() Mantener Con copia defensiva
anotarIncidencia(String) Mantener Operación de negocio legítima
empleadoEnElLimite() Mantener Sustituye a navegar hasta el empleado
getPrestamosCreados() Mantener Estadística de sesión
sePuedePrestar(Material, Empleado) Mantener Comprobación previa al caso de uso

3. Sustitución de los dos getters eliminados. Donde antes se escribía p.getMaterial().getTipo() o p.getEmpleadoNoUsar().getIdentificador(), se añaden delegaciones:

public String getTipoMaterial()          { return material.getTipo(); }
public String getReferenciaMaterial()    { return material.getReferencia(); }
public String getIdentificadorEmpleado() { return empleado.getIdentificador(); }

Son tres métodos triviales, y esa es la objeción habitual: "he cambiado un getter por tres". La respuesta es que ahora la clase controla qué se puede saber del empleado, y nadie puede llamar a registrarPrestamo() a sus espaldas. Se ha cambiado una puerta abierta por tres ventanas con reja.

4. Javadoc de los tres métodos núcleo:

/**
 * Registra la devolucion del material y aplica la multa correspondiente.
 *
 * <p>Precondiciones: {@code diasTranscurridos >= 0}. Si el prestamo ya estaba
 * devuelto, la llamada no tiene efecto.
 *
 * <p>Postcondiciones: {@code estaDevuelto()} es {@code true}; el material
 * vuelve a estar disponible; el empleado libera un prestamo activo; el valor
 * devuelto esta en el intervalo [0, {@code Material.MULTA_MAXIMA}].
 *
 * @param diasTranscurridos dias desde la entrega, nunca negativo
 * @return multa aplicada en euros
 */
public double registrarDevolucion(int diasTranscurridos) { ... }

/**
 * Calcula la multa que corresponderia si el material se devolviera ahora,
 * segun el plazo y la tarifa propios del soporte.
 *
 * <p>Precondiciones: ninguna. Puede llamarse en cualquier momento.
 * <p>Postcondiciones: no modifica el estado del prestamo; el resultado
 * esta en [0, {@code Material.MULTA_MAXIMA}].
 *
 * @return importe en euros
 */
public double calcularMulta() { ... }

/**
 * Indica si es posible registrar un prestamo del material indicado al
 * empleado indicado, sin crearlo.
 *
 * <p>Precondiciones: ninguna; acepta argumentos nulos y responde
 * {@code false}.
 * <p>Postcondiciones: no modifica ningun objeto.
 *
 * @param material material solicitado
 * @param empleado empleado solicitante
 * @return {@code true} si el material esta disponible y el empleado no ha
 *         alcanzado {@code Empleado.MAX_PRESTAMOS_SIMULTANEOS}
 */
public static boolean sePuedePrestar(Material material, Empleado empleado) { ... }

Fíjate en un detalle del tercero: documentar "acepta nulos y responde false" es una decisión de diseño. Sin ella, cada llamador tendría que comprobar por su cuenta, o descubrir el comportamiento a base de NullPointerException.

5. ReciboConsola ya usaba solo delegaciones (getTituloMaterial(), getNombreEmpleado()), así que sigue compilando sin cambios. Es la señal de que la poda era correcta: lo que se ha eliminado no lo usaba nadie legítimo.

Solución 2

El método original mezcla cuatro niveles: interacción con el usuario, validación de reglas, creación del préstamo y presentación de resultados. Se reparte así.

Dominio. La comprobación combinada ya existe (Prestamo.sePuedePrestar) y el vencimiento lo calcula el constructor. No hay que añadir reglas nuevas, solo un método que explique por qué no se puede prestar:

/**
 * @return motivo por el que no se puede prestar, o cadena vacia si si se puede
 */
public static String motivoRechazo(Material material, Empleado empleado) {
    if (material == null || empleado == null) return "Datos incompletos";
    if (!material.estaDisponible())           return "El material no esta disponible";
    if (!empleado.puedeTomarPrestado())       return "El empleado ha alcanzado su limite";
    return "";
}

Presentación. Todo el texto, en ReciboConsola:

public void imprimirCabeceraPrestamo() {
    System.out.println("--- NUEVO PRESTAMO ---");
}

public void imprimirRechazo(String motivo) {
    System.out.println("ERROR: " + motivo);
}

public void imprimirCondiciones(Material m) {
    System.out.printf("  Plazo: %d dias | Tarifa: %.2f EUR/dia | Tope: %.2f EUR%n",
                      m.getDiasPrestamo(), m.getTarifaDiaria(), Material.MULTA_MAXIMA);
}

public void imprimirPrestamoRegistrado(Prestamo p) {
    System.out.printf("OK. %s registrado. Vence el dia %d.%n",
                      p.getReferencia(), p.getDiaVencimiento());
}

Interacción con el usuario. También es presentación, así que la confirmación se aísla:

/** @return true si el usuario confirma la operacion. */
public boolean confirmar(Scanner sc) {
    System.out.print("Confirmar (si/no): ");
    return sc.nextLine().trim().equalsIgnoreCase("si");
}

Caso de uso. Coordina y nada más:

public static void procesarPrestamoCompleto(Material m, Empleado e, int dia, Scanner sc) {

    recibo.imprimirCabeceraPrestamo();

    String motivo = Prestamo.motivoRechazo(m, e);
    if (!motivo.isEmpty()) {
        recibo.imprimirRechazo(motivo);
        return;
    }

    recibo.imprimirCondiciones(m);
    if (!recibo.confirmar(sc)) {
        System.out.println("Cancelado");
        return;
    }

    recibo.imprimirPrestamoRegistrado(new Prestamo(m, e, dia));
}

Mejoras conseguidas, más allá de la longitud:

Problema original Cómo queda resuelto
m.estaDisponible() == false !motivo.isEmpty(), y la razón viene con texto
El literal 3 duplicando el límite Lo aporta Empleado.MAX_PRESTAMOS_SIMULTANEOS
El literal 20.0 duplicando el tope Material.MULTA_MAXIMA
dia + m.getDiasPrestamo() recalculado fuera Lo calcula el constructor; se lee con getDiaVencimiento()
Mensajes de error dispersos Todos en la clase de presentación
Imposible cambiar de canal Sustituir ReciboConsola por otra clase

Solución 3

1. MaterialFisico / MaterialDigital — sobreabstracción (YAGNI). Hoy los tres materiales del catálogo son físicos, así que MaterialDigital no tendría ninguna subclase y MaterialFisico no aportaría ni un campo ni un método que Material no tenga ya. Añadiría un nivel a la jerarquía —justo lo que 03-05 desaconseja— a cambio de nada. Alternativa: esperar. El día que entre un curso en vídeo bajo demanda, con reglas propias de acceso, se introduce el nivel intermedio con un caso real en la mano.

2. getCamposInternos() — abstracción con fugas, y de las graves. Un método que devuelve "el estado en bruto" publica la representación interna como parte de la API: en cuanto alguien lo use en producción "solo para un log", ya no podrás cambiar los campos sin romperlo. Además invita a parsear la salida en lugar de usar los métodos. Alternativa: un toString() bien sobrescrito, que es exactamente para esto y se estudia en la lección siguiente. La depuración se hace con el depurador (02-05), que ve los campos privados sin necesidad de exponerlos.

3. Clase ReglasNegocio con las constantes — depende, y por eso es la más interesante. Como está formulada, es discutible: las constantes ya viven en Material, que es la clase que las usa, y sacarlas a un contenedor separado las alejaría de su significado; además getDiasPrestamo() y getTarifaDiaria() son polimórficas por soporte, así que una clase única no podría representarlas sin volver a un switch sobre el tipo, es decir, deshaciendo lo logrado en 03-06. Sí estaría justificada si esos valores tuvieran que leerse de un fichero de configuración o cambiar por sede, porque entonces habría un segundo caso real. Hoy no lo hay. Veredicto: YAGNI, con fecha de revisión.

4. Parámetro boolean modoSilencioso — mal por dos razones. La primera es la del apartado de buenas prácticas de 03-03: un parámetro booleano en la llamada no dice nada (registrarDevolucion(20, true) es ilegible). La segunda es más profunda: su existencia demuestra que la abstracción está rota, porque un método de dominio no debería imprimir nada, y por tanto no debería haber nada que silenciar. Alternativa: que registrarDevolucion no escriba en consola en absoluto y devuelva la información —lo hace, devuelve la multa—, dejando que la capa de presentación decida qué mostrar y cuándo. El "modo silencioso" desaparece porque el problema desaparece.

Un patrón que conviene reconocer en las cuatro respuestas: las propuestas 2 y 4 son parches sobre síntomas; una vez separados los niveles de abstracción, ninguna de las dos tiene razón de ser.

Conclusión

Has completado los cuatro pilares. La abstracción no es una palabra clave que se teclea, sino la disciplina de quedarse con lo esencial para el problema, y por eso el mismo libro se modela distinto en una biblioteca, una tienda y una imprenta. Sabes distinguirla del encapsulamiento —qué expongo frente a cómo lo oculto— y por qué necesitas las dos. Has aprendido a detectar y corregir la mezcla de niveles de abstracción, el vicio que convertía tu main del módulo 2 en un bloque ilegible, separando las reglas de negocio en el dominio, la presentación en su propio paquete y dejando el caso de uso en dos líneas. Sabes escribir contratos en Javadoc con precondiciones, postcondiciones e invariantes, y elegir conscientemente entre validar defensivamente o documentar y confiar. Modelas con el vocabulario del dominio, de modo que un bibliotecario de Nexus Software reconocería tu código. Has comprobado que separar el qué del cómo se paga solo: cambiar la política de multas a una tarifa progresiva afecta a un fichero. Y tienes criterio sobre los dos extremos: la API pública mínima frente al coste permanente de exponer de más, las abstracciones con fugas y el YAGNI que frena las jerarquías inventadas para casos que nadie ha pedido.

También sabes lo que falta: los dos mecanismos con que Java materializa todo esto —interfaces (04-01) y clases abstractas (04-02)— llegan en el módulo siguiente, y ahora sabrás para qué sirven, que es exactamente lo que le falta a quien los aprende antes de tiempo.

Queda una lección para cerrar el módulo, y es la que hace que tus objetos se comporten como ciudadanos de primera clase del lenguaje. Ahora mismo, imprimir un Libro produce un ilegible Libro@1b6d3586, y dos ejemplares con el mismo ISBN se consideran distintos porque equals compara direcciones de memoria. En La Clase Object: equals, hashCode y toString aprenderás el papel de la raíz de toda la jerarquía, el contrato completo de equals con sus cinco propiedades, por qué hashCode debe sobrescribirse siempre junto a él y qué se rompe si no lo haces. Y cumplirás la promesa que cierra el módulo 2: las tres variables sueltas mayorRetraso, empleadoMayorRetraso y libroMayorRetraso se convertirán por fin en un único objeto coherente.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados