La lliçó anterior va acabar amb un diagnòstic: el domini de BiblioTech ja no es pot corrompre des de fora, però la superfície pública de les seves classes ha crescut sense control. Prestec exposa més de vint mètodes, entre ells un getEmpleatNoUsar() el nom del qual confessa que no hauria d'existir. Encapsular respon la pregunta com amago el que hi ha a dins; queda per respondre la més difícil: què mereix ser a fora. Aquest és el territori de l'abstracció, el quart pilar de la POO i, a diferència dels altres tres, no un mecanisme del llenguatge sinó una disciplina de disseny: quedar-se amb l'essencial per al problema que es resol i descartar tota la resta. Un mapa de metro és una abstracció magistral —menteix sobre les distàncies, ignora la geografia i tanmateix et porta a la teva destinació millor que un plànol a escala—. Aquesta lliçó t'ensenya a dissenyar així: a decidir què exposes, a no barrejar nivells dins d'un mètode, a documentar contractes i a reconèixer tant les abstraccions que degoten com el vici contrari, abstreure el que ningú no ha demanat.
Contingut
- L'abstracció com a disciplina de disseny
- Abstracció enfront d'encapsulament
- Nivells d'abstracció i per què no s'han de barrejar
- Refactorització: separar la regla de la presentació
- Disseny per contracte: precondicions, postcondicions i invariants
- El vocabulari del domini
- Separar el què del com
- API pública mínima i el cost d'exposar de més
- Abstraccions amb fuites
- Sobreabstracció i YAGNI
- Els mecanismes que falten: interfícies i classes abstractes
- Errors Habituals i Consells
- Exercicis
- L'abstracció com a disciplina de disseny
Abstreure és quedar-se amb l'essencial i descartar l'accidental. La paraula clau és "per al problema": no existeix l'abstracció correcta en absolut, només la correcta per a un propòsit.
El mateix llibre físic s'abstreu de manera diferent segons qui el modeli:
| Sistema | Què és essencial | Què es descarta |
|---|---|---|
| BiblioTech (préstecs) | Títol, ISBN, disponibilitat, termini | Pes, nombre de pàgines, color de la coberta |
| Botiga en línia | Preu, existències, imatge de coberta, enviament | Disponibilitat de préstec |
| Impremta | Gramatge del paper, tinta, format, enquadernació | ISBN, autor |
| Servei de mudances | Pes i volum | Absolutament tota la resta |
Cap no és més "vertadera" que una altra. Un Llibre a BiblioTech no té pes perquè a BiblioTech no li importa el pes, i afegir-lo "per si de cas" seria contaminar el model.
D'aquí la primera pregunta que t'has de fer davant de qualsevol classe:
Què necessita saber i fer aquest objecte per al problema que resolem? Tota la resta sobra.
- Abstracció enfront d'encapsulament
Els dos conceptes es confonen constantment perquè col·laboren, però responen preguntes diferents:
| Abstracció | Encapsulament | |
|---|---|---|
| Pregunta que respon | Què exposo? | Com ho amago? |
| Naturalesa | Decisió de disseny | Mecanisme del llenguatge |
| Moment | En modelar, abans d'escriure codi | En implementar |
| Eines | Triar classes, operacions i noms | private, protected, getters, operacions de negoci |
| S'equivoca quan... | Exposes el que no importa o amagues el que sí | Deixes portes obertes a l'estat intern |
| A BiblioTech | Decidir que Prestec ofereix registrarDevolucio(int) |
Fer private el camp multaAplicada |
| Analogia del cotxe | El volant i els pedals són l'abstracció de "conduir" | El capó tancat impedeix tocar el motor |
Es pot tenir l'una sense l'altra, i tots dos errors són reals:
- Bon encapsulament, mala abstracció: tots els camps
private, però amb trenta getters i setters validats. Res no es corromp, però la classe no significa res; és un full de càlcul amb sintaxi Java. - Bona abstracció, mal encapsulament: una API pública magnífica —
prestar(),retornar(),calcularMulta()— al costat de campspublicque permeten saltar-se-la. El disseny és correcte i no es compleix.
Necessites totes dues. L'abstracció decideix quines portes hi ha; l'encapsulament garanteix que no n'hi ha d'altres.
- Nivells d'abstracció i per què no s'han de barrejar
Un programa té capes de detall, del concepte al bit:
flowchart TD
A["Nivell 4: cas d'us
'registrar la devolucio d'un llibre'"] --> B["Nivell 3: regles de negoci
calcular retard, aplicar sostre, classificar"]
B --> C["Nivell 2: operacions de domini
material.retornar(), empleat.registrarDevolucio()"]
C --> D["Nivell 1: mecanica del llenguatge
Math.min, printf, bucles, aritmetica"]
La regla, coneguda com a principi de nivell únic d'abstracció:
Dins d'un mateix mètode, totes les sentències haurien de pertànyer aproximadament al mateix nivell.
Per què? Perquè llegir un mètode que salta de nivell obliga a canviar de marc mental a cada línia. Un mètode que diu "registra la devolució, i per cert aquí va la fórmula de l'IVA i l'amplada de columna del rebut" no es pot llegir en diagonal: cal llegir-lo sencer, cada vegada.
Aquest és l'aspecte de la barreja, i probablement en reconeguis l'estil perquè és exactament el del teu main del mòdul 2:
// MALAMENT: quatre nivells d'abstraccio en catorze linies
public void processarDevolucio(Prestec prestec, int dies) {
System.out.println("========================================"); // nivell 1
System.out.println(" REBUT DE DEVOLUCIO - BIBLIOTECH"); // nivell 1
int retard = Math.max(0, dies - prestec.getMaterial().getDiesPrestec()); // nivell 3
double multa = retard * prestec.getMaterial().getTarifaDiaria(); // nivell 3
if (multa > 20.0) { // nivell 3
multa = 20.0;
}
System.out.printf(" %-20s %s%n", "Material:", prestec.getTitolMaterial()); // nivell 1
System.out.printf(" %-20s %d dies%n", "Retard:", retard); // nivell 1
System.out.printf(" %-20s %.2f EUR%n", "Multa:", multa); // nivell 1
prestec.getMaterial().retornar(); // nivell 2
System.out.println("========================================"); // nivell 1
}Els problemes concrets, més enllà de l'estètica:
- No es pot reutilitzar la regla. Si un altre punt del programa necessita la multa, ha de copiar la fórmula o cridar aquest mètode i aguantar el rebut per consola.
- No es pot provar. Per verificar el càlcul cal capturar la sortida estàndard (mòdul 11).
- No es pot canviar el canal. Si demà el rebut és una pàgina web o un PDF, cal reescriure la lògica de negoci juntament amb el format.
- El sostre de 20 € està duplicat. Ja viu a
Material.MULTA_MAXIMA, i aquí apareix com a literal. Dues veritats per a un mateix fet. - Viola la Llei de Demeter tres vegades amb
prestec.getMaterial().getX().
- Refactorització: separar la regla de la presentació
Reescrivim el mètode anterior repartint cada línia al seu nivell.
Nivell 3, regles de negoci: ja són al domini. Prestec.registrarDevolucio(int) calcula la multa, marca el préstec, allibera el material i descompta a l'empleat (lliçó 03-07). No cal escriure res de nou.
Nivell 1, presentació: una classe a part, fora del domini.
package com.nexussoftware.bibliotech.presentacio;
import com.nexussoftware.bibliotech.domini.Prestec;
/** Genera els textos que BiblioTech mostra per consola. */
public class RebutConsola {
private static final String SEPARADOR = "========================================";
/** Imprimeix el rebut d'una devolucio ja registrada. */
public void imprimirRebut(Prestec prestec) {
System.out.println(SEPARADOR);
System.out.println(" REBUT DE DEVOLUCIO - BIBLIOTECH");
System.out.printf(" %-20s %s%n", "Referencia:", prestec.getReferencia());
System.out.printf(" %-20s %s%n", "Material:", prestec.getTitolMaterial());
System.out.printf(" %-20s %s%n", "Empleat:", prestec.getNomEmpleat());
System.out.printf(" %-20s %d dies%n", "Retard:", prestec.calcularDiesRetard());
System.out.printf(" %-20s %.2f EUR%n", "Multa:", prestec.getMultaAplicada());
System.out.printf(" %-20s %s%n", "Gravetat:", prestec.classificarGravetat());
System.out.println(SEPARADOR);
}
}Nivell 4, el cas d'ús: coordina, no calcula ni imprimeix.
/** Registra la devolucio d'un prestec i lliura el rebut a l'empleat. */
public void processarDevolucio(Prestec prestec, int diesTranscorreguts) {
prestec.registrarDevolucio(diesTranscorreguts);
rebut.imprimirRebut(prestec);
}Dues línies. I cadascuna al seu nivell.
Compara l'abans i el després:
| Criteri | Abans | Després |
|---|---|---|
| Nivells barrejats en un mètode | 4 | 1 |
| On és la fórmula de la multa | Duplicada al mètode | Només a Material |
El literal 20.0 |
Escrit a mà | MULTA_MAXIMA |
| Canviar a sortida web | Reescriure el mètode | Una altra classe de presentació |
| Provar el càlcul | Capturant System.out |
Cridant calcularMulta() |
| Violacions de Demeter | 3 | 0 |
| Línies al cas d'ús | 14 | 2 |
Nota el paquet nou, presentacio. L'estructura de BiblioTech comença a reflectir els seus nivells d'abstracció:
com.nexussoftware.bibliotech ├── BiblioTechApp.java arrencada ├── domini/ QUE es el negoci i les seves regles ├── presentacio/ COM es mostra └── servei/ (modul 5) casos d'us
Aquesta separació és la llavor de l'arquitectura per capes que veuràs completa al mòdul 12.
- Disseny per contracte: precondicions, postcondicions i invariants
Una abstracció és una promesa: "crida'm així i et retornaré això". Com més explícita sigui la promesa, menys marge hi ha per als malentesos. El disseny per contracte formalitza aquesta promesa en tres parts:
| Element | Què és | Qui ho garanteix |
|---|---|---|
| Precondició | El que ha de ser cert abans de cridar | Qui crida |
| Postcondició | El que serà cert després de la crida | El mètode |
| Invariant | El que és cert sempre, abans i després | La classe |
En Java no hi ha sintaxi per als contractes (altres llenguatges sí que en tenen), així que es documenten amb Javadoc, que ja coneixes del mòdul 1:
/**
* Registra la devolucio del material prestat i aplica la multa que correspongui.
*
* <p><b>Precondicions:</b>
* <ul>
* <li>{@code diesTranscorreguts >= 0}.</li>
* <li>El prestec no ha d'estar ja retornat; si ho esta, la crida no
* te efecte i es retorna la multa aplicada anteriorment.</li>
* </ul>
*
* <p><b>Postcondicions:</b>
* <ul>
* <li>{@code estaRetornat()} passa a ser {@code true}.</li>
* <li>El material torna a estar disponible.</li>
* <li>L'empleat te un prestec actiu menys.</li>
* <li>El valor retornat esta entre 0 i {@code Material.MULTA_MAXIMA}.</li>
* </ul>
*
* <p><b>Invariant de classe mantingut:</b> {@code diesTranscorreguts >= 0} i
* {@code diaVenciment == diaPrestec + material.getDiesPrestec()}.
*
* @param diesTranscorreguts dies transcorreguts des del lliurament, mai negatiu
* @return import de la multa aplicada, en euros
*/
public double registrarDevolucio(int diesTranscorreguts) { ... }Avantatges d'escriure-ho:
- Qui crida sap què ha de complir sense llegir la implementació. Això és, literalment, la definició d'abstracció.
- T'obliga a tu, en escriure-ho, a pensar en els casos límit: què passa si ja estava retornat? I si els dies són negatius? Molts contractes revelen forats de disseny abans que existeixin com a errors.
- L'IDE el mostra en autocompletar, així que el contracte viatja amb el codi.
I una guia pràctica sobre precondicions: hi ha dues estratègies, i convé triar conscientment.
| Estratègia | Com | Quan |
|---|---|---|
| Defensiva | Comprovar i rebutjar (avisos ara, excepcions al mòdul 6) | API pública, entrada de l'usuari |
| Per contracte estricte | Documentar la precondició i assumir que es compleix | Mètodes private, codi intern |
Comprovar-ho tot a tot arreu multiplica el codi sense aportar seguretat: si un mètode private només el criden dos mètodes de la mateixa classe que ja van validar, tornar a validar és soroll.
- El vocabulari del domini
Una bona abstracció parla l'idioma del negoci. Si un bibliotecari de Nexus Software llegís el teu codi per damunt de l'espatlla, hauria de reconèixer les paraules.
Compara:
// Vocabulari tecnic: no significa res per al negoci
DataManager dm = new DataManager();
dm.process(item, 20);
if (dm.getStatus() == 2) { ... }
// Vocabulari del domini
double multa = prestec.registrarDevolucio(20);
if (prestec.estaVencut()) { ... }A la idea de fer servir un únic vocabulari compartit entre el codi, la documentació i les converses amb el negoci se l'anomena llenguatge ubic. La seva regla pràctica és simple: si el negoci diu "multa", el codi diu multa, no penaltyAmount, fee ni charge.
El vocabulari de BiblioTech, que fas servir des del mòdul 1, és exactament això: titol, autor, isbn, disponible, diesRetard, multa, gravetat, referencia, prestar, retornar. Cap paraula tècnica no s'ha colat al domini, i per això prestec.registrarDevolucio(20) s'entén sense obrir la classe.
- Separar el què del com
El resultat tangible d'una bona abstracció és que qui crida deixa de saber com es fan les coses.
Recorre el que ha passat amb el càlcul de la multa al llarg del curs:
| Moment | Qui sap com es calcula |
|---|---|
| Mòdul 1 | main: la fórmula està escrita allà |
| Mòdul 2 | main: la fórmula, el sostre i la classificació, dins d'un switch |
| Lliçó 03-03 | Prestec: main crida calcularMulta() |
| Lliçó 03-05 | Material i les seves subclasses: cada suport aporta la seva tarifa |
| Lliçó 03-07 | Només el domini: main ni tan sols pot tocar el resultat |
| Ara | main ni tan sols menciona la paraula "multa" llevat que sigui per mostrar-la |
La prova definitiva que el què i el com estan separats és aquesta: pots canviar la implementació sense tocar els que criden? Si Nexus Software decideix demà que la tarifa sigui progressiva —0,25 € els set primers dies i 0,50 € a partir del vuitè—, quants fitxers cal tocar?
// Regla nova, nomes a Material
@Override
public double calcularMulta(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts);
int tramLleu = Math.min(retard, LLINDAR_LLEU);
int tramGreu = Math.max(0, retard - LLINDAR_LLEU);
double total = tramLleu * getTarifaDiaria()
+ tramGreu * getTarifaDiaria() * 2;
return Math.min(total, MULTA_MAXIMA);
}Un fitxer. Ni main, ni el rebut, ni Prestec se n'assabenten. Això és el que compra l'abstracció, i és la raó per la qual es paga el seu cost en indirecció.
- API pública mínima i el cost d'exposar de més
L'API pública d'una classe és tot el que altres poden fer servir: mètodes i camps public, i els seus constructors. És la seva promesa cap a l'exterior.
I aquesta promesa té un cost que gairebé ningú no calcula en escriure el codi:
| Cost | Explicació |
|---|---|
| Compatibilitat | Cada mètode públic és un compromís: si l'esborres o en canvies la signatura, trenques tots els seus usuaris |
| Càrrega cognitiva | Una classe amb 25 mètodes públics obliga a llegir 25 noms per saber quin fer servir |
| Superfície d'error | Cada mètode públic és una via per la qual algú pot fer servir malament la classe |
| Llibertat perduda | No pots canviar el que has promès; només el que és privat és lliure |
La regla d'or:
És fàcil afegir un mètode públic més tard; és caríssim treure'l. Davant del dubte, deixa'l
private.
Aplicat a Prestec, revisem la seva API actual amb un criteri sever:
| Membre | Públic? | Raó |
|---|---|---|
registrarDevolucio(int) |
Sí | És el cas d'ús |
calcularMulta(), calcularDiesRetard(), classificarGravetat() |
Sí | Consultes que necessita la presentació |
getReferencia(), getTitolMaterial(), getNomEmpleat() |
Sí | Identifiquen el préstec a la pantalla |
estaRetornat(), estaVencut(), getDiaVenciment() |
Sí | Estat consultat en llistats |
getMultaAplicada() |
Sí | Fet registrat després de la devolució |
setDiesTranscorreguts(int) |
No → private |
És un detall intern de registrarDevolucio |
getEmpleatNoUsar() |
No → eliminar | Permet mutar l'empleat per la porta del darrere |
getMaterial() |
Discutible | Necessari en alguns llistats; es pot substituir per delegacions |
getDiaPrestec(), diesRestants() |
Sí | Es mostren al resum |
empleatAlLimit() |
Sí | Consulta de negoci legítima |
esPotPrestar(Material, Empleat) |
Sí | Comprovació prèvia al cas d'ús |
Amb aquesta poda, Prestec passa de més de vint membres públics a catorze, i cap no permet descompensar el sistema. L'exercici 1 et demana portar-la fins al final.
- Abstraccions amb fuites
Una abstracció amb fuites (leaky abstraction) és la que promet amagar un detall però obliga a conèixer-lo igualment. La llei enunciada per Joel Spolsky diu, amb una mica d'humor negre, que totes les abstraccions no trivials degoten: la qüestió no és evitar-ho del tot, sinó no agreujar-ho.
Exemples que ja has vist o veuràs:
| Fuita | Com es nota |
|---|---|
Prestec.getMaterial() retorna l'objecte mutable |
Qui el rep pot cridar retornar() saltant-se el préstec |
Un getIncidencies() sense còpia defensiva |
Qui crida descobreix que el seu array és l'intern |
Un mètode anomenat guardar() que de vegades falla per xarxa |
L'abstracció "guardar" no pot amagar que hi ha una xarxa a sota (mòdul 9) |
String.substring i l'ús de memòria |
En versions antigues de Java, una subcadena retenia tot l'array original |
Com reduir les fuites:
- Retorna tipus immutables o còpies en lloc d'estat intern.
- No exposis tipus de la implementació a la signatura pública. Si demà vols canviar un array per una llista, la teva signatura no t'hauria d'obligar a trencar ningú.
- Documenta el que degota quan no ho puguis evitar. Una fuita documentada és un contracte; una fuita silenciosa és un parany.
- Sobreabstracció i YAGNI
El vici oposat també existeix, i en mans d'algú que acaba de descobrir la POO és el més freqüent: abstreure el que ningú no ha demanat.
Símptomes de sobreabstracció:
- Jerarquies de cinc nivells per a tres casos concrets.
- Classes anomenades
AbstractBaseGenericProcessorFactory. - Paràmetres de configuració que sempre valen el mateix.
- Punts d'extensió "per si algun dia" que no es fan servir mai.
- Una interfície per a cada classe, amb una única implementació.
El principi que ho conté s'anomena YAGNI: You Aren't Gonna Need It, "no ho necessitaràs". Formulat com a regla:
No afegeixis flexibilitat fins que tinguis dos casos reals que la requereixin. Amb un, és endevinar.
Aplicat a BiblioTech, un contrast honest:
| Abstracció | Justificada? | Per què |
|---|---|---|
Material amb tres subclasses |
Sí | Hi ha tres suports reals amb regles diferents |
Separar domini de presentacio |
Sí | El mòdul 12 portarà una interfície web |
getDiesPrestec() sobreescrivible |
Sí | Cada suport té el seu termini, comprovat |
Una jerarquia MaterialFisic / MaterialDigital |
No, encara | No hi ha cap material digital al catàleg |
| Un sistema de tarifes configurable des de fitxer | No | Hi ha una tarifa per suport i no canvia |
Interfície Prestable amb un sol implementador |
No | No aporta res fins que n'hi hagi un segon |
I un advertiment sobre l'equilibri: sobreabstreure és un error car però visible; infraabstreure produeix el main de dues-centes línies del mòdul 2, que també ho és. La resposta professional no és triar un extrem, sinó refactoritzar quan apareix el segon cas. És exactament el que has fet: Material no existia fins que van aparèixer revistes i DVD.
- Els mecanismes que falten: interfícies i classes abstractes
Tota aquesta lliçó ha parlat d'abstracció com a disciplina: què modelar, què exposar, com separar nivells. Però Java ofereix a més dos mecanismes del llenguatge dissenyats específicament per materialitzar-la, i tots dos arriben al mòdul següent:
| Mecanisme | Què aporta | Lliçó |
|---|---|---|
| Interfícies | Declaren què es pot fer sense dir com; una classe en pot implementar diverses | 04-01 |
| Classes abstractes | Classes que no es poden instanciar i que poden declarar mètodes sense cos, obligant les subclasses a implementar-los | 04-02 |
Dues mancances concretes del disseny actual que aquests mecanismes resoldran:
Primera: Material es pot instanciar. Avui res no impedeix escriure new Material("Alguna cosa", "REF-1", true), i això no significa res: a la biblioteca hi ha llibres, revistes i DVD, no "materials genèrics". Una classe abstracta ho prohibeix a nivell de compilador.
Segona: getTipus() retorna "Material" per defecte. Si algú crea una subclasse nova i oblida sobreescriure'l, apareixerà "Material" als llistats sense que ningú ho detecti. Un mètode abstracte converteix aquest oblit en un error de compilació:
// Avancament de la llico 04-02
public abstract class Material {
public abstract String getTipus(); // sense cos: obliga a implementar-lo
}Queda't amb això: al mòdul 4 aprendràs com s'escriuen; en aquesta lliçó has après per què existeixen. L'ordre importa, perquè qui aprèn interface sense haver entès l'abstracció acaba creant una interfície per classe sense saber per a què.
Errors Habituals i Consells
- Confondre abstracció amb encapsulament. Abstracció és què exposo; encapsulament és com ho amago. La taula de l'apartat 2 els separa.
- Barrejar nivells en un mètode. Regles de negoci i
printfal mateix bloc és l'error més freqüent en codi de principiant, i el més car de desfer. - Ficar presentació a les classes de domini. Un
Llibreque sap imprimir la seva fitxa per consola no es podrà reutilitzar en un web. La presentació va en un altre paquet. - Fer públic "per si de cas". Cada mètode públic és un compromís permanent. Comença
privatei puja només quan algú ho necessiti de debò. - Noms genèrics.
Manager,Processor,Helper,Data,Info,Util. Si no pots posar un nom concret a una classe, probablement encara no saps què és. - Documentar el com en lloc del què. Un Javadoc que diu "recorre l'array i suma" queda obsolet tan bon punt canviïs la implementació. Documenta el contracte: què rep, què retorna, què garanteix.
- Abstreure sense un segon cas. YAGNI. Espera que aparegui el segon suport, la segona base de dades, el segon format.
- Consell: descriu el mètode en una frase. Si necessites "i" o "a més", el mètode fa dues coses i probablement barreja nivells.
- Consell: llegeix el teu codi com si fossis el bibliotecari. Si
p.registrarDevolucio(20)s'entén idm.process(i, 20)no, ja saps quin està ben anomenat. - Consell: la prova del canvi. Davant de cada decisió de disseny, pregunta't què caldria tocar si la regla canviés. Com menys fitxers, millor l'abstracció.
Exercicis
Exercici 1: redissenyar l'API pública de Prestec
Aquest és l'exercici central de la lliçó. Pren la classe Prestec tal com va quedar a 03-07 i redueix la seva superfície pública al mínim necessari.
- Llista tots els seus membres públics actuals.
- Per a cadascun, decideix: mantenir, fer
privateo eliminar, justificant la decisió en una frase. - Elimina
getEmpleatNoUsar()i substitueix-ne els usos per delegacions adequades. - Escriu el Javadoc complet, amb precondicions i postcondicions, dels tres mètodes que consideris el nucli de la classe.
- Comprova que
BiblioTechAppiRebutConsolacontinuen funcionant amb l'API reduïda.
Exercici 2: separar nivells d'abstracció
Refactoritza aquest mètode repartint cada línia al seu nivell. Ha de quedar: les regles al domini, la presentació a RebutConsola i el cas d'ús reduït a tres o quatre línies llegibles.
public static void processarPrestecComplet(Material m, Empleat e, int dia, Scanner sc) {
System.out.println("--- NOU PRESTEC ---");
if (m.estaDisponible() == false) {
System.out.println("ERROR: no disponible");
return;
}
if (e.getPrestecsAcumulats() >= 3) {
System.out.println("ERROR: limit assolit");
return;
}
System.out.print("Confirmar (si/no): ");
String r = sc.nextLine().trim();
if (!r.equalsIgnoreCase("si")) {
System.out.println("Cancellat");
return;
}
Prestec p = new Prestec(m, e, dia);
int venc = dia + m.getDiesPrestec();
System.out.println("OK. Venc el dia " + venc
+ ". Tarifa de retard: " + m.getTarifaDiaria() + " EUR/dia"
+ ". Sostre: " + 20.0 + " EUR");
}Exercici 3: jutjar abstraccions
Per a cada proposta d'un company, decideix si està justificada, si és sobreabstracció (YAGNI) o si és una abstracció amb fuites. Justifica-ho en dues o tres frases i, quan escaigui, proposa l'alternativa.
- Crear
MaterialFisiciMaterialDigitalcom a nivells intermedis entreMateriali les tres classes actuals. - Afegir a
Materialun mètodepublic String[] getCampsInterns()que retorni l'estat en brut "per depurar". - Extreure les quatre constants de negoci a una classe
ReglesNegociamb mètodesstatic. - Afegir a
Prestecun paràmetreboolean modeSilenciosaregistrarDevolucioper no imprimir avisos.
Solucions
Solució 1
1 i 2. Revisió membre per membre:
| Membre | Decisió | Justificació |
|---|---|---|
Prestec(Material, Empleat, int, int) |
Mantenir | Constructor canònic |
Prestec(Material, Empleat, int) |
Mantenir | Alta habitual, amb zero dies |
getReferencia() |
Mantenir | Identifica el préstec a la pantalla i als avisos |
getTitolMaterial() |
Mantenir | Delegació conforme a Demeter |
getNomEmpleat() |
Mantenir | Delegació conforme a Demeter |
getMaterial() |
Eliminar | Exposa un col·laborador mutable; se substitueix per delegacions |
getEmpleatNoUsar() |
Eliminar | Permet mutar l'empleat saltant-se el préstec |
getDiaPrestec() |
Mantenir | Es mostra al resum |
getDiaVenciment() |
Mantenir | Dada clau per a l'usuari |
getDiesTranscorreguts() |
Mantenir | Es mostra i es consulta |
setDiesTranscorreguts(int) |
A private |
Detall intern de registrarDevolucio |
estaRetornat() |
Mantenir | Estat consultat en llistats |
estaVencut() |
Mantenir | Consulta de negoci |
diesRestants() |
Mantenir | Es mostra en avisos de venciment |
calcularDiesRetard() |
Mantenir | La presentació ho necessita |
calcularMulta() |
Mantenir | Consulta prèvia a la devolució |
classificarGravetat() |
Mantenir | Es mostra al rebut |
getMultaAplicada() |
Mantenir | Fet registrat després de retornar |
getIncidencies() |
Mantenir | Amb còpia defensiva |
anotarIncidencia(String) |
Mantenir | Operació de negoci legítima |
empleatAlLimit() |
Mantenir | Substitueix navegar fins a l'empleat |
getPrestecsCreats() |
Mantenir | Estadística de sessió |
esPotPrestar(Material, Empleat) |
Mantenir | Comprovació prèvia al cas d'ús |
3. Substitució dels dos getters eliminats. On abans s'escrivia p.getMaterial().getTipus() o p.getEmpleatNoUsar().getIdentificador(), s'afegeixen delegacions:
public String getTipusMaterial() { return material.getTipus(); }
public String getReferenciaMaterial() { return material.getReferencia(); }
public String getIdentificadorEmpleat() { return empleat.getIdentificador(); }Són tres mètodes trivials, i aquesta és l'objecció habitual: "he canviat un getter per tres". La resposta és que ara la classe controla què es pot saber de l'empleat, i ningú no pot cridar registrarPrestec() a la seva esquena. S'ha canviat una porta oberta per tres finestres amb reixa.
4. Javadoc dels tres mètodes nucli:
/**
* Registra la devolucio del material i aplica la multa corresponent.
*
* <p>Precondicions: {@code diesTranscorreguts >= 0}. Si el prestec ja estava
* retornat, la crida no te efecte.
*
* <p>Postcondicions: {@code estaRetornat()} es {@code true}; el material
* torna a estar disponible; l'empleat allibera un prestec actiu; el valor
* retornat esta a l'interval [0, {@code Material.MULTA_MAXIMA}].
*
* @param diesTranscorreguts dies des del lliurament, mai negatiu
* @return multa aplicada en euros
*/
public double registrarDevolucio(int diesTranscorreguts) { ... }
/**
* Calcula la multa que correspondria si el material es retornes ara,
* segons el termini i la tarifa propis del suport.
*
* <p>Precondicions: cap. Es pot cridar en qualsevol moment.
* <p>Postcondicions: no modifica l'estat del prestec; el resultat
* esta a [0, {@code Material.MULTA_MAXIMA}].
*
* @return import en euros
*/
public double calcularMulta() { ... }
/**
* Indica si es possible registrar un prestec del material indicat a
* l'empleat indicat, sense crear-lo.
*
* <p>Precondicions: cap; accepta arguments nuls i respon
* {@code false}.
* <p>Postcondicions: no modifica cap objecte.
*
* @param material material sollicitat
* @param empleat empleat sollicitant
* @return {@code true} si el material esta disponible i l'empleat no ha
* assolit {@code Empleat.MAX_PRESTECS_SIMULTANIS}
*/
public static boolean esPotPrestar(Material material, Empleat empleat) { ... }Fixa't en un detall del tercer: documentar "accepta nuls i respon false" és una decisió de disseny. Sense ella, cada crida hauria de comprovar pel seu compte, o descobrir el comportament a base de NullPointerException.
5. RebutConsola ja feia servir només delegacions (getTitolMaterial(), getNomEmpleat()), així que continua compilant sense canvis. És el senyal que la poda era correcta: el que s'ha eliminat no ho feia servir ningú legítim.
Solució 2
El mètode original barreja quatre nivells: interacció amb l'usuari, validació de regles, creació del préstec i presentació de resultats. Es reparteix així.
Domini. La comprovació combinada ja existeix (Prestec.esPotPrestar) i el venciment el calcula el constructor. No cal afegir regles noves, només un mètode que expliqui per què no es pot prestar:
/**
* @return motiu pel qual no es pot prestar, o cadena buida si si que es pot
*/
public static String motiuRebuig(Material material, Empleat empleat) {
if (material == null || empleat == null) return "Dades incompletes";
if (!material.estaDisponible()) return "El material no esta disponible";
if (!empleat.potPrendrePrestat()) return "L'empleat ha assolit el seu limit";
return "";
}Presentació. Tot el text, a RebutConsola:
public void imprimirCapcaleraPrestec() {
System.out.println("--- NOU PRESTEC ---");
}
public void imprimirRebuig(String motiu) {
System.out.println("ERROR: " + motiu);
}
public void imprimirCondicions(Material m) {
System.out.printf(" Termini: %d dies | Tarifa: %.2f EUR/dia | Sostre: %.2f EUR%n",
m.getDiesPrestec(), m.getTarifaDiaria(), Material.MULTA_MAXIMA);
}
public void imprimirPrestecRegistrat(Prestec p) {
System.out.printf("OK. %s registrat. Venc el dia %d.%n",
p.getReferencia(), p.getDiaVenciment());
}Interacció amb l'usuari. També és presentació, així que la confirmació s'aïlla:
/** @return true si l'usuari confirma l'operacio. */
public boolean confirmar(Scanner sc) {
System.out.print("Confirmar (si/no): ");
return sc.nextLine().trim().equalsIgnoreCase("si");
}Cas d'ús. Coordina i res més:
public static void processarPrestecComplet(Material m, Empleat e, int dia, Scanner sc) {
rebut.imprimirCapcaleraPrestec();
String motiu = Prestec.motiuRebuig(m, e);
if (!motiu.isEmpty()) {
rebut.imprimirRebuig(motiu);
return;
}
rebut.imprimirCondicions(m);
if (!rebut.confirmar(sc)) {
System.out.println("Cancellat");
return;
}
rebut.imprimirPrestecRegistrat(new Prestec(m, e, dia));
}Millores aconseguides, més enllà de la longitud:
| Problema original | Com queda resolt |
|---|---|
m.estaDisponible() == false |
!motiu.isEmpty(), i la raó ve amb text |
El literal 3 duplicant el límit |
L'aporta Empleat.MAX_PRESTECS_SIMULTANIS |
El literal 20.0 duplicant el sostre |
Material.MULTA_MAXIMA |
dia + m.getDiesPrestec() recalculat fora |
El calcula el constructor; es llegeix amb getDiaVenciment() |
| Missatges d'error dispersos | Tots a la classe de presentació |
| Impossible canviar de canal | Substituir RebutConsola per una altra classe |
Solució 3
1. MaterialFisic / MaterialDigital — sobreabstracció (YAGNI). Avui els tres materials del catàleg són físics, així que MaterialDigital no tindria cap subclasse i MaterialFisic no aportaria ni un camp ni un mètode que Material no tingui ja. Afegiria un nivell a la jerarquia —justament el que 03-05 desaconsella— a canvi de res. Alternativa: esperar. El dia que entri un curs en vídeo sota demanda, amb regles pròpies d'accés, s'introdueix el nivell intermedi amb un cas real a la mà.
2. getCampsInterns() — abstracció amb fuites, i de les greus. Un mètode que retorna "l'estat en brut" publica la representació interna com a part de l'API: tan bon punt algú el faci servir en producció "només per a un log", ja no podràs canviar els camps sense trencar-lo. A més convida a analitzar la sortida en lloc de fer servir els mètodes. Alternativa: un toString() ben sobreescrit, que és exactament per a això i s'estudia a la lliçó següent. La depuració es fa amb el depurador (02-05), que veu els camps privats sense necessitat d'exposar-los.
3. Classe ReglesNegoci amb les constants — depèn, i per això és la més interessant. Tal com està formulada, és discutible: les constants ja viuen a Material, que és la classe que les fa servir, i treure-les a un contenidor separat les allunyaria del seu significat; a més getDiesPrestec() i getTarifaDiaria() són polimòrfiques per suport, així que una classe única no les podria representar sense tornar a un switch sobre el tipus, és a dir, desfent el que s'ha aconseguit a 03-06. Sí que estaria justificada si aquests valors s'haguessin de llegir d'un fitxer de configuració o canviar per seu, perquè llavors hi hauria un segon cas real. Avui no n'hi ha. Veredicte: YAGNI, amb data de revisió.
4. Paràmetre boolean modeSilencios — malament per dues raons. La primera és la de l'apartat de bones pràctiques de 03-03: un paràmetre booleà a la crida no diu res (registrarDevolucio(20, true) és il·legible). La segona és més profunda: la seva existència demostra que l'abstracció està trencada, perquè un mètode de domini no hauria d'imprimir res, i per tant no hi hauria d'haver res a silenciar. Alternativa: que registrarDevolucio no escrigui a la consola en absolut i retorni la informació —ho fa, retorna la multa—, deixant que la capa de presentació decideixi què mostrar i quan. El "mode silenciós" desapareix perquè el problema desapareix.
Un patró que convé reconèixer a les quatre respostes: les propostes 2 i 4 són pedaços sobre símptomes; un cop separats els nivells d'abstracció, cap de les dues no té raó de ser.
Conclusió
Has completat els quatre pilars. L'abstracció no és una paraula clau que es tecleja, sinó la disciplina de quedar-se amb l'essencial per al problema, i per això el mateix llibre es modela diferent en una biblioteca, una botiga i una impremta. Saps distingir-la de l'encapsulament —què exposo enfront de com ho amago— i per què necessites totes dues. Has après a detectar i corregir la barreja de nivells d'abstracció, el vici que convertia el teu main del mòdul 2 en un bloc il·legible, separant les regles de negoci al domini, la presentació al seu propi paquet i deixant el cas d'ús en dues línies. Saps escriure contractes en Javadoc amb precondicions, postcondicions i invariants, i triar conscientment entre validar defensivament o documentar i confiar. Modeles amb el vocabulari del domini, de manera que un bibliotecari de Nexus Software reconeixeria el teu codi. Has comprovat que separar el què del com es paga sol: canviar la política de multes a una tarifa progressiva afecta un fitxer. I tens criteri sobre els dos extrems: l'API pública mínima enfront del cost permanent d'exposar de més, les abstraccions amb fuites i el YAGNI que frena les jerarquies inventades per a casos que ningú no ha demanat.
També saps el que falta: els dos mecanismes amb què Java materialitza tot això —interfícies (04-01) i classes abstractes (04-02)— arriben al mòdul següent, i ara sabràs per a què serveixen, que és exactament el que li falta a qui els aprèn abans d'hora.
Queda una lliçó per tancar el mòdul, i és la que fa que els teus objectes es comportin com a ciutadans de primera classe del llenguatge. Ara mateix, imprimir un Llibre produeix un il·legible Llibre@1b6d3586, i dos exemplars amb el mateix ISBN es consideren diferents perquè equals compara adreces de memòria. A La classe Object: equals, hashCode i toString aprendràs el paper de l'arrel de tota la jerarquia, el contracte complet d'equals amb les seves cinc propietats, per què hashCode s'ha de sobreescriure sempre al seu costat i què es trenca si no ho fas. I compliràs la promesa que tanca el mòdul 2: les tres variables soltes retardMesGran, empleatRetardMesGran i llibreRetardMesGran es convertiran per fi en un únic objecte coherent.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
