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
- La abstracción como disciplina de diseño
- Abstracción frente a encapsulamiento
- Niveles de abstracción y por qué no deben mezclarse
- Refactorización: separar la regla de la presentación
- Diseño por contrato: precondiciones, postcondiciones e invariantes
- El vocabulario del dominio
- Separar el qué del cómo
- API pública mínima y el coste de exponer de más
- Abstracciones con fugas
- Sobreabstracción y YAGNI
- Los mecanismos que faltan: interfaces y clases abstractas
- Errores Comunes y Consejos
- Ejercicios
- 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.
- 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 campospublicque 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.
- 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:
- 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.
- No se puede probar. Para verificar el cálculo hay que capturar la salida estándar (módulo 11).
- 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.
- El tope de 20 € está duplicado. Ya vive en
Material.MULTA_MAXIMA, y aquí aparece como literal. Dos verdades para un mismo hecho. - Viola la Ley de Demeter tres veces con
prestamo.getMaterial().getX().
- 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.
- 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.
- 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.
- 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.
- 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) |
Sí | Es el caso de uso |
calcularMulta(), calcularDiasRetraso(), clasificarGravedad() |
Sí | Consultas que necesita la presentación |
getReferencia(), getTituloMaterial(), getNombreEmpleado() |
Sí | Identifican el préstamo en pantalla |
estaDevuelto(), estaVencido(), getDiaVencimiento() |
Sí | Estado consultado en listados |
getMultaAplicada() |
Sí | Hecho registrado tras la devolución |
setDiasTranscurridos(int) |
No → private |
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() |
Sí | Se muestran en el resumen |
empleadoEnElLimite() |
Sí | Consulta de negocio legítima |
sePuedePrestar(Material, Empleado) |
Sí | 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.
- 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.
- 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 |
Sí | Hay tres soportes reales con reglas distintas |
Separar dominio de presentacion |
Sí | El módulo 12 traerá una interfaz web |
getDiasPrestamo() sobrescribible |
Sí | 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.
- 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
printfen 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
Libroque 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
privatey 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 ydm.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.
- Lista todos sus miembros públicos actuales.
- Para cada uno, decide: mantener, hacer
privateo eliminar, justificando la decisión en una frase. - Elimina
getEmpleadoNoUsar()y sustituye sus usos por delegaciones adecuadas. - Escribe el Javadoc completo, con precondiciones y postcondiciones, de los tres métodos que consideres el núcleo de la clase.
- Comprueba que
BiblioTechAppyReciboConsolasiguen 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.
- Crear
MaterialFisicoyMaterialDigitalcomo niveles intermedios entreMaterialy las tres clases actuales. - Añadir a
Materialun métodopublic String[] getCamposInternos()que devuelva el estado en bruto "para depurar". - Extraer las cuatro constantes de negocio a una clase
ReglasNegociocon métodosstatic. - Añadir a
Prestamoun parámetroboolean modoSilenciosoenregistrarDevolucionpara 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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
