Tot el mòdul hem anat arrossegant un deute, i ha arribat el moment de pagar-lo. Els camps de Llibre, Empleat, Prestec i Material continuen sent public, i això vol dir que qualsevol línia de qualsevol fitxer pot escriure llibre.disponible = true saltant-se els avisos de prestar(), o empleat.prestecsAcumulats = -7, o assignar a un préstec una multa inventada que no correspon a cap càlcul. Tota la feina dels constructors validant dades s'ensorra tan bon punt algú pot escriure directament als camps. L'encapsulament és el pilar que tanca aquesta porta. Però atenció: encapsular no és "posar un getter i un setter a cada camp" —aquesta recepta mecànica, tan repetida, deixa l'objecte tan exposat com abans amb més línies de codi—. Encapsular és amagar la representació interna i exposar només operacions que preservin les regles del negoci. Aquesta lliçó t'ensenya la diferència, que és la que separa un model de dades anèmic d'un model de domini de debò.
Contingut
- Què és realment encapsular
- Els quatre modificadors d'accés
- Taula completa de visibilitat
- Getters i setters: el que la recepta mecànica no explica
- Invariants de classe
- Operacions de negoci en lloc de setters
- Immutabilitat
- El perill de retornar referències mutables: còpia defensiva
- Encapsulament a nivell de paquet
- La Llei de Demeter
- BiblioTech: el domini encapsulat
- Errors Habituals i Consells
- Exercicis
- Què és realment encapsular
Encapsular és separar el que un objecte ofereix de com ho fa, de manera que l'interior es pugui canviar sense trencar ningú.
Pensa en un caixer automàtic. La seva interfície pública és: introduir targeta, teclejar el PIN, demanar un import, recollir bitllets. El que hi ha a dins —la disposició dels cassets, l'ordre en què es dispensen els bitllets, el protocol amb el banc— no només està amagat, és que no t'ha d'importar. Si demà canvien els cassets, tu continues traient diners igual.
Aplicat al codi, encapsular bé produeix tres beneficis concrets:
| Benefici | Què significa a la pràctica |
|---|---|
| Invariants garantits | Si prestecsAcumulats només pot canviar per mètodes, és impossible que sigui negatiu |
| Llibertat per canviar l'interior | Pots substituir un camp per un altre càlcul sense tocar els usuaris de la classe |
| Superfície petita d'errors | Si alguna cosa va malament a l'estat d'un objecte, el culpable és dins de la seva classe, no en 40 fitxers |
El segon benefici és el més subestimat, i mereix un exemple. Suposa que Prestec guarda un camp multa:
Si un dia decideixes que la multa no s'ha d'emmagatzemar sinó calcular sempre a partir dels dies, has de modificar els vint fitxers. En canvi, si des del principi exposes un mètode:
public double getMulta() {
return Math.min(calcularDiesRetard() * getTarifaDiaria(), MULTA_MAXIMA);
}...pots canviar la implementació tantes vegades com vulguis: els vint fitxers continuen cridant getMulta() sense assabentar-se'n. El mètode és un contracte; el camp és una filtració.
- Els quatre modificadors d'accés
Java ofereix quatre nivells de visibilitat. Tres tenen paraula reservada i un és el que s'aplica quan no escrius res:
| Modificador | Paraula clau | Idea en una frase |
|---|---|---|
| Privat | private |
Només dins de la classe mateixa |
| De paquet | (cap) | Dins del mateix paquet |
| Protegit | protected |
Mateix paquet i subclasses, siguin on siguin |
| Públic | public |
Des de qualsevol lloc |
public class Llibre {
private String titol; // nomes Llibre
String notaInterna; // classes del paquet domini
protected boolean disponible; // paquet domini + subclasses de Llibre
public String getTitol() { return titol; } // tothom
}S'apliquen a classes, camps, mètodes i constructors. Amb un matís: una classe de nivell superior només pot ser public o de paquet; private i protected no hi tenen sentit (sí que en tenen a les classes internes, lliçó 04-03).
- Taula completa de visibilitat
Aquesta és la taula que convé tenir a mà fins que s'interioritzi:
| Modificador | Mateixa classe | Mateix paquet | Subclasse en un altre paquet | Qualsevol classe |
|---|---|---|---|---|
private |
Sí | No | No | No |
| (default) | Sí | Sí | No | No |
protected |
Sí | Sí | Sí | No |
public |
Sí | Sí | Sí | Sí |
Dos matisos que es pregunten molt:
Sobre protected. Inclou la visibilitat de paquet: un membre protected és visible per a totes les classes del mateix paquet, siguin o no subclasses. I des d'una subclasse en un altre paquet, només s'hi accedeix a través d'una referència del tipus de la subclasse, no de la superclasse. És una regla subtil que rarament molesta a la pràctica.
Sobre private i les classes imbricades. Una classe pot accedir als membres private d'una altra classe imbricada dins d'ella, i a la inversa: la unitat d'encapsulament en Java és el fitxer de la classe de nivell superior, no cada classe individual. Ho veuràs a 04-03.
Criteri professional de partida:
Declara-ho tot
privateper defecte. Puja la visibilitat només quan tinguis una raó concreta, i documenta aquesta raó.
En particular, desconfia de protected als camps: converteix cada subclasse present i futura en un client que depèn de la teva representació interna, i és exactament l'escenari de la fragilitat de la classe base que vas veure a 03-05. Si una subclasse necessita alguna cosa, prefereix donar-li un mètode protected.
- Getters i setters: el que la recepta mecànica no explica
La recepta que s'ensenya a tot arreu és: camps private, i per a cadascun un getX() i un setX(). L'IDE fins i tot els genera sols. I el resultat és aquest:
public class Empleat {
private String nom;
private int prestecsAcumulats;
public String getNom() { return nom; }
public void setNom(String n) { this.nom = n; }
public int getPrestecsAcumulats() { return prestecsAcumulats; }
public void setPrestecsAcumulats(int p) { this.prestecsAcumulats = p; }
}Pregunta't què s'ha guanyat respecte a tenir els camps públics. La resposta honesta: gairebé res.
empleat.setPrestecsAcumulats(-7); // continua sent possible
empleat.setPrestecsAcumulats(9999); // se salta el limit de 3
empleat.setNom(null); // adeu a la validacio del constructorUn setter sense validació és un camp públic amb dues línies més. Pitjor encara: dona la falsa sensació d'estar encapsulant.
El criteri correcte és preguntar-se, camp per camp, dues coses:
- Algú de fora necessita llegir aquesta dada? Si no, no hi ha getter.
- Té sentit que algú de fora la canviï a un valor arbitrari? Si no —i gairebé mai no en té—, no hi ha setter: hi ha una operació de negoci.
Aplicat a BiblioTech:
| Camp | Getter? | Setter? | Per què |
|---|---|---|---|
Llibre.titol |
Sí | No | El títol d'un llibre no canvia |
Llibre.isbn |
Sí | No | És la seva identitat |
Material.disponible |
Sí, com a estaDisponible() |
No | Canvia només mitjançant prestar() / retornar() |
Empleat.nom |
Sí | No | Dada identitària |
Empleat.prestecsAcumulats |
Sí | No | Canvia mitjançant registrarPrestec() / registrarDevolucio() |
Prestec.diesTranscorreguts |
Sí | Sí, validat | Sí que evoluciona amb el temps |
Prestec.multa |
Sí, calculat | Mai | És una conseqüència, no una dada |
Aquesta última fila mereix aturar-s'hi. Si existís setMulta(double):
prestec.setMulta(0.0); // "perdono" una multa de 20 EUR sense que ningu ho sapiga
prestec.setMulta(1000.0); // cobro mil euros per un retard de tres diesTota la política de multes de Nexus Software —tarifa diària, sostre de 20 €, saturació a zero— quedaria anul·lada per una línia. La regla es converteix en un suggeriment.
- Invariants de classe
Un invariant és una condició que s'ha de complir sempre durant tota la vida de l'objecte, des que el constructor acaba fins que es recull. Són les regles que defineixen què significa que un objecte sigui vàlid.
Invariants de BiblioTech:
| Classe | Invariant |
|---|---|
Material |
titol mai no és null ni està en blanc |
Material |
referencia mai no és null |
Empleat |
0 <= prestecsAcumulats <= MAX_PRESTECS_SIMULTANIS |
Prestec |
diesTranscorreguts >= 0 |
Prestec |
diaVenciment == diaPrestec + material.getDiesPrestec() |
Prestec |
0 <= multa calculada <= MULTA_MAXIMA |
L'encapsulament és el mecanisme que els fa complir, i funciona en dos fronts:
- El constructor estableix els invariants en néixer (03-04).
- Els mètodes són els únics que poden modificar l'estat, i per tant els únics responsables de mantenir-los.
Si algun camp és públic, no hi ha invariants: només hi ha bons desitjos.
Quan un mutador sí que és necessari, ha de validar:
/**
* Actualitza els dies transcorreguts des que es va fer el prestec.
* @param dies nombre de dies, mai negatiu
*/
public void setDiesTranscorreguts(int dies) {
if (dies < 0) {
System.out.println("AVIS: dies negatius (" + dies + "). S'ignora l'actualitzacio.");
return; // es preserva l'invariant
}
this.diesTranscorreguts = dies;
}Igual que a 03-04: la forma correcta de rebutjar seria llançar una excepció, i arribarà al mòdul 6.
- Operacions de negoci en lloc de setters
Aquest és el canvi de mentalitat clau de la lliçó. En comptes de preguntar-te "quins camps té aquesta classe i com els exposo?", pregunta't "què li pot passar a aquest objecte al negoci real?".
A BiblioTech, a un préstec li passen coses molt concretes: es registra la seva devolució, es prorroga, es consulta el seu estat. No li passa "que li assignin una multa".
Abans, amb la recepta mecànica:
// Qui crida calcula, decideix i assigna: la regla viu FORA de l'objecte
int retard = dies - 15;
if (retard < 0) retard = 0;
double multa = retard * 0.25;
if (multa > 20.0) multa = 20.0;
prestec.setDiesRetard(retard);
prestec.setMulta(multa);
prestec.setRetornat(true);
llibre.setDisponible(true);
empleat.setPrestecsAcumulats(empleat.getPrestecsAcumulats() - 1);Cinc crides que s'han de fer totes i en ordre. Si qui crida n'oblida una, el sistema queda incoherent: un préstec retornat el llibre del qual continua marcat com a prestat.
Després, amb una operació de negoci:
Una sola crida, impossible de deixar a mitges:
/**
* Registra la devolucio del material despres del nombre de dies indicat.
* Calcula la multa segons les regles vigents, allibera el material i
* descompta el prestec a l'empleat.
*
* @param diesTranscorreguts dies des del lliurament, mai negatiu
* @return import de la multa aplicada, entre 0 i MULTA_MAXIMA
*/
public double registrarDevolucio(int diesTranscorreguts) {
if (retornat) {
System.out.println("AVIS: el prestec " + referencia + " ja estava retornat.");
return multaAplicada;
}
setDiesTranscorreguts(diesTranscorreguts); // valida l'invariant
this.multaAplicada = material.calcularMulta(this.diesTranscorreguts);
this.retornat = true;
material.retornar(); // el material torna al prestatge
empleat.registrarDevolucio(); // l'empleat allibera un forat
return multaAplicada;
}Compara les dues versions en una taula:
| Criteri | Cinc setters | registrarDevolucio(int) |
|---|---|---|
| On viu la regla de la multa | A qui crida (i a cada crida) | A Prestec, una sola vegada |
| Es pot deixar a mitges | Sí | No |
| Es pot falsejar la multa | Sí | No |
Què es llegeix a main |
Aritmètica | Una frase del negoci |
| Canviar la tarifa afecta | Tots els que criden | Una constant |
Aquesta és la diferència entre un model anèmic (objectes que només guarden dades, amb la lògica a fora) i un model de domini ric (objectes que protegeixen les seves regles). El primer és POO només en la sintaxi.
- Immutabilitat
Un objecte immutable és aquell l'estat del qual no pot canviar després de construït. S'aconsegueix amb quatre regles:
- Tots els camps
private final. - Cap setter ni mètode que modifiqui l'estat.
- La classe declarada
final, perquè cap subclasse no hi afegeixi mutabilitat. - Si algun camp és una referència mutable, no s'exposa directament (apartat 8).
Avantatges, i n'hi ha molts:
| Avantatge | Explicació |
|---|---|
| Impossible corrompre l'estat | No hi ha manera de deixar-lo invàlid després del constructor |
| Segur entre fils | Sense escriptures, no hi ha condicions de cursa (mòdul 8) |
| Es pot compartir sense por | Copiar i compartir passen a ser equivalents |
| Bon candidat a clau | El seu hashCode no canvia mai (lliçó 03-09 i mòdul 5) |
| Més fàcil de raonar | Si el vas veure vàlid un cop, ho serà sempre |
String és l'exemple canònic, i ja en vas patir les conseqüències al mòdul 1: text.toUpperCase() no canvia text, retorna una altra cadena.
Per què Llibre és un bon candidat a immutable? Perquè les seves dades bibliogràfiques —títol, autor, ISBN, any— no canvien mai. Un llibre publicat el 2018 no passarà a ser-ho el 2019, i el seu ISBN és la seva identitat. L'única cosa que canvia és la disponibilitat, i aquesta és una propietat de l'exemplar prestat, discutiblement fins i tot d'una altra classe.
Un Llibre completament immutable es veuria així:
public final class LlibreImmutable {
private final String titol;
private final String autor;
private final String isbn;
private final int anyPublicacio;
public LlibreImmutable(String titol, String autor, String isbn, int anyPublicacio) {
this.titol = titol;
this.autor = autor;
this.isbn = isbn;
this.anyPublicacio = anyPublicacio;
}
public String getTitol() { return titol; }
public String getAutor() { return autor; }
public String getIsbn() { return isbn; }
public int getAnyPublicacio() { return anyPublicacio; }
/** "Modificar" un immutable significa crear-ne un altre. */
public LlibreImmutable ambTitol(String nouTitol) {
return new LlibreImmutable(nouTitol, autor, isbn, anyPublicacio);
}
}Fixa't en ambTitol: al món immutable no es modifica, es deriva un objecte nou. És el mateix patró de String.toUpperCase().
Nota sobre
record. Escriure una classe immutable així és tan repetitiu que Java 16 va incorporar els records:public record LlibreImmutable(String titol, String autor, String isbn, int anyPublicacio) { }genera automàticament els campsprivate final, el constructor, els accessors, i a mésequals,hashCodeitoString. S'estudien a la lliçó 04-07. Escriu primer la versió manual: entendre què genera elrecordés més útil que fer-lo servir a cegues.
A BiblioTech mantindrem disponible mutable —l'exemplar es presta i es retorna— però tota la resta final. És una immutabilitat parcial, i és un compromís raonable: minimitza el que pot canviar.
- El perill de retornar referències mutables: còpia defensiva
Aquí hi ha la fuita d'encapsulament més subtil i la que més codi "aparentment correcte" arruïna. Observa:
public class Prestec {
private final String[] incidencies; // array: solucio provisional (modul 5)
public String[] getIncidencies() {
return incidencies; // FUITA!
}
}El camp és private. El camp és final. I tot i així:
String[] fora = prestec.getIncidencies();
fora[0] = "Incidencia inventada"; // s'ha modificat l'interior del prestecfinal protegeix la referència, no l'objecte apuntat (ho vas veure a 03-02). En retornar l'array, has lliurat a l'exterior una clau de l'interior del teu objecte. L'invariant ja no està garantit.
La solució és la còpia defensiva: retornar una còpia, no l'original.
/**
* @return copia de les incidencies registrades; modificar-la no afecta el prestec
*/
public String[] getIncidencies() {
return java.util.Arrays.copyOf(incidencies, incidencies.length);
}El mateix perill existeix a l'entrada. Si un constructor guarda un array que li passen, qui crida conserva una referència a aquest array:
// Constructor ingenu
public Prestec(String[] incidencies) {
this.incidencies = incidencies; // qui crida pot continuar modificant-lo
}
// Constructor amb copia defensiva
public Prestec(String[] incidencies) {
this.incidencies = (incidencies == null)
? new String[0]
: java.util.Arrays.copyOf(incidencies, incidencies.length);
}Regla general:
Copia defensivament en rebre i en retornar qualsevol objecte mutable que formi part de l'estat de la teva classe.
Quan no cal? Quan l'objecte és immutable. String, Integer o un LlibreImmutable es poden retornar directament sense risc, perquè ningú no els pot alterar. És un altre argument a favor de la immutabilitat: elimina tota una categoria de precaucions.
I una matisació important: Prestec.getMaterial() retorna un Material mutable (el seu disponible canvia). Caldria copiar-lo? No, i aquí hi ha el matís: el material no és una part interna del préstec, és una entitat compartida del catàleg. Que dos préstecs i el catàleg vegin el mateix objecte és precisament el que volem. La còpia defensiva s'aplica al que és part de l'objecte (composició), no al que és un col·laborador (associació). Torna a la taula de relacions de la lliçó 03-01: aquesta distinció, que allà semblava teòrica, té aquí una conseqüència pràctica directa.
- Encapsulament a nivell de paquet
L'encapsulament no acaba a la classe. Els paquets són la segona barrera, i l'organització actual de BiblioTech ja l'anticipa:
com.nexussoftware.bibliotech
├── BiblioTechApp.java arrencada i presentacio
├── domini/
│ ├── Material.java regles de negoci
│ ├── Llibre.java
│ ├── Revista.java
│ ├── Dvd.java
│ ├── Empleat.java
│ └── Prestec.java
└── servei/ (a partir del modul 5)
└── GestorPrestecs.java orquestracio de casos d'usEl criteri: públic només el que un altre paquet necessiti de debò. Una classe auxiliar que només fa servir el domini es pot declarar sense public, i llavors és invisible des de fora. Això et deixa llibertat total per canviar-la, perquè tens la certesa que ningú aliè no la fa servir.
Java 9 va portar aquesta idea un nivell més amunt amb el sistema de mòduls i module-info.java, que permet declarar quins paquets exporta una llibreria sencera. S'estudia a la lliçó 10-06.
- La Llei de Demeter
La Llei de Demeter, o principi de mínim coneixement, es resumeix en una frase:
Parla només amb els teus amics immediats, no amb els amics dels teus amics.
En codi: evita les cadenes de punts que travessen diversos objectes.
// MALAMENT: main coneix l'estructura interna de Prestec I de Material
String titol = prestec.getMaterial().getTitol();
if (prestec.getEmpleat().getPrestecsAcumulats() >= 3) { ... }
// BE: cada objecte respon per si mateix
String titol = prestec.getTitolMaterial();
if (prestec.empleatAlLimit()) { ... }Per què importa? Perquè prestec.getMaterial().getTitol() acobla main amb dues classes: si Material canvia getTitol() per una altra cosa, cal tocar tots els llocs que encadenaven. Amb prestec.getTitolMaterial(), el canvi afecta només Prestec.
Dues precisions per no aplicar-la com un dogma:
- No comptis punts:
System.out.println(...)osb.append(a).append(b)encadenen i estan perfectament bé. El que la llei desaconsella és navegar per l'estructura aliena per prendre decisions. - Portar-la a l'extrem genera munts de mètodes delegats trivials. Aplica-la on l'acoblament faci mal: a les decisions de negoci.
- BiblioTech: el domini encapsulat
Refactoritzem les classes del projecte. Aquest és l'estat en què queden.
Material
package com.nexussoftware.bibliotech.domini;
/** Material prestable del cataleg de Nexus Software. */
public class Material {
public static final int DIES_PRESTEC = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int LLINDAR_LLEU = 7;
private static int materialsCreats = 0;
private final String titol;
private final String referencia;
private boolean disponible;
public Material(String titol, String referencia, boolean disponible) {
this.titol = (titol == null || titol.isBlank()) ? "Sense titol" : titol.trim();
this.referencia = (referencia == null || referencia.isBlank())
? "000-0000000000" : referencia.trim();
this.disponible = disponible;
materialsCreats++;
}
// --- Consultes (getters nomes on tenen sentit) ---
public String getTitol() { return titol; }
public String getReferencia() { return referencia; }
public boolean estaDisponible(){ return disponible; }
public static int getMaterialsCreats() { return materialsCreats; }
// --- Parametres que cada suport redefineix ---
public int getDiesPrestec() { return DIES_PRESTEC; }
public double getTarifaDiaria() { return TARIFA_DIARIA; }
public String getTipus() { return "Material"; }
// --- Regles de negoci ---
public int calcularDiesRetard(int diesTranscorreguts) {
return Math.max(0, diesTranscorreguts - getDiesPrestec());
}
public double calcularMulta(int diesTranscorreguts) {
return Math.min(calcularDiesRetard(diesTranscorreguts) * getTarifaDiaria(),
MULTA_MAXIMA);
}
public String classificarGravetat(int diesTranscorreguts) {
int retard = calcularDiesRetard(diesTranscorreguts);
if (retard == 0) return "SENSE RETARD";
if (retard <= LLINDAR_LLEU) return "LLEU";
return "GREU";
}
// --- Operacions de negoci (NO hi ha setDisponible) ---
/** @return true si el material s'ha pogut prestar. */
public boolean prestar() {
if (!disponible) {
System.out.println("AVIS: '" + titol + "' ja estava prestat.");
return false;
}
disponible = false;
return true;
}
/** @return true si el material s'ha pogut retornar. */
public boolean retornar() {
if (disponible) {
System.out.println("AVIS: '" + titol + "' ja estava disponible.");
return false;
}
disponible = true;
return true;
}
public String descriure() {
return titol + " (ref. " + referencia + ")";
}
}Nota el canvi a prestar() i retornar(): ara retornen boolean en lloc de void. Així qui crida pot saber si l'operació ha tingut efecte, sense necessitat de llegir el camp.
I nota el que no hi ha: setTitol, setReferencia ni setDisponible. La disponibilitat només canvia per les dues operacions del negoci.
Llibre
package com.nexussoftware.bibliotech.domini;
/** Llibre tecnic del cataleg. Les seves dades bibliografiques son immutables. */
public class Llibre extends Material {
private final String autor;
private final int anyPublicacio;
public Llibre(String titol, String autor, String isbn,
int anyPublicacio, boolean disponible) {
super(titol, isbn, disponible);
this.autor = (autor == null || autor.isBlank()) ? "Desconegut" : autor.trim();
this.anyPublicacio = (anyPublicacio < 1450 || anyPublicacio > 2100)
? 0 : anyPublicacio;
}
public Llibre(String titol, String autor, String isbn, int anyPublicacio) {
this(titol, autor, isbn, anyPublicacio, true);
}
public String getAutor() { return autor; }
public int getAnyPublicacio() { return anyPublicacio; }
/** L'ISBN es la referencia d'un llibre. */
public String getIsbn() { return getReferencia(); }
@Override public String getTipus() { return "Llibre"; }
@Override
public String descriure() {
return super.descriure() + " - " + autor + ", " + anyPublicacio;
}
}Observa que Llibre accedeix al títol mitjançant getTitol() heretat, no directament: el camp de Material és private i ni tan sols la seva subclasse el pot tocar. Això permetria a Material canviar la seva representació interna demà sense trencar Llibre.
Empleat
package com.nexussoftware.bibliotech.domini;
/** Empleat de Nexus Software autoritzat a prendre materials en prestec. */
public class Empleat {
public static final int MAX_PRESTECS_SIMULTANIS = 3;
private static int empleatsRegistrats = 0;
private final String nom;
private final String identificador;
private int prestecsAcumulats; // invariant: 0..MAX
public Empleat(String nom, String identificador, int prestecsAcumulats) {
this.nom = (nom == null || nom.isBlank())
? "Empleat sense nom" : nom.trim();
this.identificador = (identificador == null || !identificador.startsWith("EMP-"))
? "EMP-000" : identificador.trim();
this.prestecsAcumulats = Math.max(0,
Math.min(prestecsAcumulats, MAX_PRESTECS_SIMULTANIS));
empleatsRegistrats++;
}
public Empleat(String nom, String identificador) {
this(nom, identificador, 0);
}
public String getNom() { return nom; }
public String getIdentificador() { return identificador; }
public int getPrestecsAcumulats() { return prestecsAcumulats; }
public boolean potPrendrePrestat() { return prestecsAcumulats < MAX_PRESTECS_SIMULTANIS; }
public static int getEmpleatsRegistrats() { return empleatsRegistrats; }
/** @return true si s'ha pogut registrar el prestec. */
public boolean registrarPrestec() {
if (!potPrendrePrestat()) {
System.out.printf("AVIS: %s ja te %d prestecs (maxim %d).%n",
nom, prestecsAcumulats, MAX_PRESTECS_SIMULTANIS);
return false;
}
prestecsAcumulats++;
return true;
}
public void registrarDevolucio() {
prestecsAcumulats = Math.max(0, prestecsAcumulats - 1);
}
public String getInicials() {
StringBuilder sb = new StringBuilder();
for (String part : nom.trim().split(" ")) {
if (!part.isEmpty()) {
sb.append(part.charAt(0)).append('.');
}
}
return sb.toString().toUpperCase();
}
}L'invariant 0 <= prestecsAcumulats <= MAX està garantit als tres únics punts on el camp canvia: el constructor (amb Math.max/Math.min), registrarPrestec() (amb la guarda) i registrarDevolucio() (amb Math.max). No hi ha una quarta porta. Això és encapsulament.
Prestec
package com.nexussoftware.bibliotech.domini;
import java.util.Arrays;
/** Registre d'un prestec d'un material a un empleat. */
public class Prestec {
private static int prestecsCreats = 0;
private final Material material;
private final Empleat empleat;
private final int diaPrestec;
private final int diaVenciment;
private final String referencia;
private int diesTranscorreguts;
private boolean retornat;
private double multaAplicada;
private String[] incidencies; // array provisional (modul 5)
public Prestec(Material material, Empleat empleat,
int diaPrestec, int diesTranscorreguts) {
this.material = (material != null) ? material
: new Llibre("Material desconegut", "Desconegut", "000-0000000000", 0, false);
this.empleat = (empleat != null) ? empleat
: new Empleat("Empleat desconegut", "EMP-000");
this.diaPrestec = Math.max(0, diaPrestec);
this.diaVenciment = this.diaPrestec + this.material.getDiesPrestec();
this.diesTranscorreguts = Math.max(0, diesTranscorreguts);
this.retornat = false;
this.multaAplicada = 0.0;
this.incidencies = new String[0];
prestecsCreats++;
this.referencia = String.format("PR-%04d", prestecsCreats);
this.material.prestar();
this.empleat.registrarPrestec();
}
public Prestec(Material material, Empleat empleat, int diaPrestec) {
this(material, empleat, diaPrestec, 0);
}
// ---------- Consultes ----------
public String getReferencia() { return referencia; }
public Material getMaterial() { return material; } // collaborador compartit
public Empleat getEmpleatNoUsar() { return empleat; } // vegeu la nota mes avall
public int getDiaPrestec() { return diaPrestec; }
public int getDiaVenciment() { return diaVenciment; }
public int getDiesTranscorreguts() { return diesTranscorreguts; }
public boolean estaRetornat() { return retornat; }
/** Delegacions que compleixen la Llei de Demeter. */
public String getTitolMaterial() { return material.getTitol(); }
public String getNomEmpleat() { return empleat.getNom(); }
/** @return copia defensiva: modificar-la no afecta el prestec. */
public String[] getIncidencies() {
return Arrays.copyOf(incidencies, incidencies.length);
}
public static int getPrestecsCreats() { return prestecsCreats; }
// ---------- Regles derivades (calculades, mai emmagatzemades) ----------
public int calcularDiesRetard() { return material.calcularDiesRetard(diesTranscorreguts); }
public double calcularMulta() { return material.calcularMulta(diesTranscorreguts); }
public String classificarGravetat() { return material.classificarGravetat(diesTranscorreguts); }
public boolean estaVencut() { return calcularDiesRetard() > 0; }
public int diesRestants() { return Math.max(0, diaVenciment - (diaPrestec + diesTranscorreguts)); }
// ---------- Operacions de negoci ----------
/**
* Actualitza els dies transcorreguts. Unic mutador, i validat.
* @param dies dies des del lliurament; els valors negatius s'ignoren
*/
public void setDiesTranscorreguts(int dies) {
if (dies < 0) {
System.out.println("AVIS: dies negatius a " + referencia + ". S'ignora.");
return;
}
this.diesTranscorreguts = dies;
}
/**
* Registra la devolucio: calcula i fixa la multa segons les regles
* vigents, allibera el material i descompta el prestec a l'empleat.
*
* @param diesTranscorreguts dies des del lliurament
* @return multa aplicada, entre 0 i MULTA_MAXIMA
*/
public double registrarDevolucio(int diesTranscorreguts) {
if (retornat) {
System.out.println("AVIS: " + referencia + " ja estava retornat.");
return multaAplicada;
}
setDiesTranscorreguts(diesTranscorreguts);
this.multaAplicada = calcularMulta();
this.retornat = true;
material.retornar();
empleat.registrarDevolucio();
return multaAplicada;
}
/** @return multa efectivament aplicada; 0 mentre no s'hagi retornat. */
public double getMultaAplicada() { return multaAplicada; }
/** Anota una incidencia del prestec (trencaments, pagines soltes...). */
public void anotarIncidencia(String text) {
if (text == null || text.isBlank()) {
return;
}
String[] ampliat = Arrays.copyOf(incidencies, incidencies.length + 1);
ampliat[incidencies.length] = text.trim();
this.incidencies = ampliat; // el modul 5 fara aixo molt millor
}
public boolean empleatAlLimit() { return !empleat.potPrendrePrestat(); }
}Sobre
getEmpleatNoUsar. Aquest nom deliberadament lleig assenyala una decisió de disseny: exposar l'Empleatsencer permet a qualsevol ferprestec.getEmpleat().registrarPrestec()i descompensar el sistema. Per això el préstec ofereixgetNomEmpleat()iempleatAlLimit()al seu lloc. A la lliçó 03-08 reduiràs formalment la superfície pública d'aquesta classe, i aquest getter serà un dels primers a desaparèixer.
Ús des de BiblioTechApp
Llibre javaEficac = new Llibre("Java Eficac", "Joshua Bloch", "978-0000000001", 2018);
Empleat marta = new Empleat("Marta Ruiz", "EMP-001");
Prestec p = new Prestec(javaEficac, marta, 100, 0);
System.out.printf("%s: '%s' a %s, venc el dia %d%n",
p.getReferencia(), p.getTitolMaterial(),
p.getNomEmpleat(), p.getDiaVenciment());
System.out.println("Disponible despres de prestar: " + javaEficac.estaDisponible());
p.anotarIncidencia("Pagina 42 subratllada");
double multa = p.registrarDevolucio(20);
System.out.printf("Retornat amb %d dies de retard. Multa: %.2f EUR (%s)%n",
p.calcularDiesRetard(), multa, p.classificarGravetat());
System.out.println("Disponible despres de retornar: " + javaEficac.estaDisponible());
System.out.println("Prestecs de Marta: " + marta.getPrestecsAcumulats());
// Intents de trencar els invariants des de fora:
// javaEficac.disponible = true; // NO COMPILA: camp private
// p.multaAplicada = 0.0; // NO COMPILA: camp private
// marta.prestecsAcumulats = 99; // NO COMPILA: camp private
p.setDiesTranscorreguts(-5); // avisa i s'ignoraSortida:
PR-0001: 'Java Eficac' a Marta Ruiz, venc el dia 115 Disponible despres de prestar: false Retornat amb 5 dies de retard. Multa: 1,25 EUR (LLEU) Disponible despres de retornar: true Prestecs de Marta: 0 AVIS: dies negatius a PR-0001. S'ignora.
Les tres línies comentades són l'important d'aquesta lliçó: ja no compilen. Les regles de Nexus Software han deixat de dependre de la bona voluntat de qui fa servir les classes.
Errors Habituals i Consells
- Confondre encapsulament amb "posar getters i setters". Un setter públic sense validació deixa l'objecte tan obert com un camp públic. Encapsular és decidir quines operacions tenen sentit, no automatitzar accessos.
- Generar tots els accessors amb l'IDE sense pensar. El generador no sap quins camps han de ser immutables. Genera i després esborra el que no hagi d'existir.
- Retornar una referència mutable interna. L'error de l'apartat 8.
finalno protegeix l'objecte apuntat. - Fer servir
protectedals camps "per si de cas una subclasse ho necessita". Converteix la representació interna en API pública per a totes les subclasses. Prefereix un mètodeprotected. - Guardar dades derivades. Si
multaes pot calcular a partir dels dies, calcula-la. Una dada duplicada és una dada que es pot desincronitzar. (APrestecguardemmultaAplicadanomés després de la devolució, perquè llavors és un fet registrat, no un càlcul viu.) - Cadenes llargues de getters.
a.getB().getC().getD()acobla el teu codi a tres classes. Delega. - Fer
publicuna classe auxiliar del domini. Si només la fa servir el seu paquet, deixa-la sense modificador. - Consell: escriu la classe de fora cap a dins. Primer decideix quines operacions oferirà, després quins camps necessita per complir-les. A l'inrevés s'acaba amb getters de tot.
- Consell: si un camp no canvia, fes-lo
finalavui. És la manera més barata de documentar i garantir un invariant. - Consell: prova de comentar un getter. Si el projecte continua compilant, esborra'l. L'API pública més segura és la més petita.
Exercicis
Exercici 1: encapsular Reserva
Reprèn la classe Reserva de la lliçó 03-04 (sala, sol·licitant, hora d'inici, hora de fi, assistents, activa, codi) i encapsula-la correctament:
- Fes tots els camps
private, ifinalels que no hagin de canviar. - Decideix, camp per camp, si necessita getter, justificant-ho en un comentari.
- Substitueix qualsevol setter per operacions de negoci:
cancellar(),ampliarUnaHora()icanviarAssistents(int)(que ha de rebutjar valors fora de rang). - Enumera al Javadoc els invariants de la classe i comprova que cap operació no els pugui trencar.
Exercici 2: detectar i tancar una fuita d'encapsulament
Aquest codi té tres fuites d'encapsulament diferents. Identifica-les, explica com s'explotaria cadascuna des de fora i corregeix-les:
public class HistorialEmpleat {
private final String nom;
private final String[] referenciesPrestecs;
private int totalMultes;
public HistorialEmpleat(String nom, String[] referenciesPrestecs) {
this.nom = nom;
this.referenciesPrestecs = referenciesPrestecs;
}
public String[] getReferenciesPrestecs() {
return referenciesPrestecs;
}
public void setTotalMultes(int totalMultes) {
this.totalMultes = totalMultes;
}
}Exercici 3: de model anèmic a model ric
Un company ha escrit aquesta lògica a BiblioTechApp fent servir setters. Refactoritza-la movent cada regla a la classe de domini que correspongui, de manera que a main quedi una sola crida. Indica quins mètodes nous apareixen i a quina classe.
// A main
if (marta.getPrestecsAcumulats() < 3 && javaEficac.estaDisponible()) {
javaEficac.setDisponible(false);
marta.setPrestecsAcumulats(marta.getPrestecsAcumulats() + 1);
Prestec p = new Prestec();
p.setMaterial(javaEficac);
p.setEmpleat(marta);
p.setDiaPrestec(100);
p.setDiaVenciment(100 + 15);
System.out.println("Prestec registrat");
} else {
System.out.println("No es pot prestar");
}Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
/**
* Reserva d'una sala de reunions de Nexus Software.
*
* Invariants garantits durant tota la vida de l'objecte:
* - 0 <= horaInici <= 23
* - horaInici < horaFi <= 23
* - assistents >= 1
* - codi no nul i unic
*/
public class Reserva {
private static int reservesCreades = 0;
private final String sala; // sense setter: canviar de sala es una altra reserva
private final Empleat sollicitant; // sense setter: identitat de la reserva
private final int horaInici; // sense setter: es fixa en reservar
private final String codi; // sense setter: el genera el sistema
private int horaFi; // muta nomes per ampliarUnaHora()
private int assistents; // muta nomes per canviarAssistents()
private boolean activa; // muta nomes per cancellar()
public Reserva(String sala, Empleat sollicitant,
int horaInici, int horaFi, int assistents) {
this.sala = (sala == null || sala.isBlank()) ? "Sala sense nom" : sala.trim();
this.sollicitant = (sollicitant != null) ? sollicitant
: new Empleat("Empleat desconegut", "EMP-000");
this.horaInici = (horaInici < 0 || horaInici > 23) ? 9 : horaInici;
if (horaFi <= this.horaInici || horaFi > 23) {
this.horaFi = Math.min(23, this.horaInici + 1);
} else {
this.horaFi = horaFi;
}
this.assistents = Math.max(1, assistents);
this.activa = true;
reservesCreades++;
this.codi = String.format("RES-%04d", reservesCreades);
}
// --- Consultes ---
public String getCodi() { return codi; } // el demana l'usuari
public String getSala() { return sala; } // es mostra en llistats
public int getHoraInici() { return horaInici; } // es mostra
public int getHoraFi() { return horaFi; } // es mostra
public int getAssistents() { return assistents; } // es mostra
public boolean estaActiva() { return activa; } // es consulta en llistar
public int duracioHores() { return horaFi - horaInici; }
/** Delegacio: evita que l'exterior navegui fins a l'Empleat. */
public String getNomSollicitant() { return sollicitant.getNom(); }
// NO hi ha getSollicitant(): exposaria un objecte mutable del domini.
// --- Operacions de negoci (cap setter) ---
/** @return true si la reserva s'ha cancellat ara. */
public boolean cancellar() {
if (!activa) {
System.out.println("AVIS: la reserva " + codi + " ja estava cancellada.");
return false;
}
activa = false;
return true;
}
/** @return true si s'ha pogut ampliar sense sortir del dia. */
public boolean ampliarUnaHora() {
if (!activa) {
System.out.println("AVIS: no es pot ampliar una reserva cancellada.");
return false;
}
if (horaFi >= 23) {
System.out.println("AVIS: la reserva " + codi + " ja arriba al final del dia.");
return false;
}
horaFi++; // l'invariant horaInici < horaFi es mante
return true;
}
/** @return true si el canvi d'assistents s'ha aplicat. */
public boolean canviarAssistents(int nous) {
if (nous < 1) {
System.out.println("AVIS: nombre d'assistents invalid (" + nous + ").");
return false;
}
assistents = nous;
return true;
}
}Justificació de les decisions clau: sala, sollicitant, horaInici i codi són final perquè canviar-los convertiria la reserva en una de diferent; horaFi, assistents i activa muten, però només a través d'operacions que validen, així que els tres invariants del Javadoc es mantenen en tot moment. I no existeix getSollicitant(): exposar l'Empleat permetria a qualsevol alterar-lo des d'una reserva, que és exactament l'acoblament que la Llei de Demeter desaconsella.
Solució 2
Fuita 1: el constructor guarda l'array rebut.
String[] refs = { "PR-0001", "PR-0002" };
HistorialEmpleat h = new HistorialEmpleat("Marta Ruiz", refs);
refs[0] = "REFERENCIA FALSA"; // s'ha modificat l'interior d'hQui crida conserva una referència al mateix array que ara és estat intern de l'objecte.
Fuita 2: el getter retorna l'array intern.
Ni private ni final protegeixen: final impedeix reassignar la variable, no modificar l'array.
Fuita 3: setTotalMultes permet qualsevol valor.
És un setter sense validació sobre una dada acumulativa que només hauria de créixer mitjançant operacions del negoci.
Versió corregida:
package com.nexussoftware.bibliotech.domini;
import java.util.Arrays;
/**
* Historial de prestecs d'un empleat.
* Invariants: referenciesPrestecs mai no es null; totalMultes >= 0.
*/
public class HistorialEmpleat {
private final String nom;
private final String[] referenciesPrestecs;
private int totalMultes;
public HistorialEmpleat(String nom, String[] referenciesPrestecs) {
this.nom = (nom == null || nom.isBlank()) ? "Desconegut" : nom.trim();
// COPIA DEFENSIVA a l'entrada: qui crida ja no arriba al nostre estat.
this.referenciesPrestecs = (referenciesPrestecs == null)
? new String[0]
: Arrays.copyOf(referenciesPrestecs, referenciesPrestecs.length);
this.totalMultes = 0;
}
public String getNom() { return nom; }
public int getTotalMultes() { return totalMultes; }
public int getNombrePrestecs() { return referenciesPrestecs.length; }
/** @return copia defensiva; modificar-la no afecta l'historial. */
public String[] getReferenciesPrestecs() {
return Arrays.copyOf(referenciesPrestecs, referenciesPrestecs.length);
}
/**
* Suma una multa a l'historial. Substitueix el setter: el total nomes
* pot creixer, i nomes amb imports valids.
*
* @param quantitat import positiu en euros
* @return true si s'ha acumulat
*/
public boolean acumularMulta(int quantitat) {
if (quantitat <= 0) {
System.out.println("AVIS: import de multa invalid (" + quantitat + ").");
return false;
}
totalMultes += quantitat;
return true;
}
}Comprovació que les fuites estan tancades:
String[] refs = { "PR-0001", "PR-0002" };
HistorialEmpleat h = new HistorialEmpleat("Marta Ruiz", refs);
refs[0] = "REFERENCIA FALSA"; // ja no afecta
h.getReferenciesPrestecs()[1] = "UNA ALTRA DE FALSA"; // modifica una copia
h.acumularMulta(-500); // rebutjat amb avis
System.out.println(Arrays.toString(h.getReferenciesPrestecs())); // [PR-0001, PR-0002]
System.out.println(h.getTotalMultes()); // 0Un apunt de rendiment: copiar a cada crida té un cost. Quan importi, les alternatives són retornar una vista de només lectura (List.copyOf, mòdul 5) o fer immutable el contingut. La regla de partida continua sent copiar: la correcció primer, l'optimització després i només amb mesures.
Solució 3
El fragment comprova precondicions, muta tres objectes i calcula el venciment des de fora. Tot això pertany al domini.
La regla completa "es pot prestar aquest material a aquest empleat?" té dues parts que ja viuen a les seves classes (estaDisponible() i potPrendrePrestat()), i falta un lloc on combinar-les. Com que el Prestec és qui les relaciona, s'hi afegeix un mètode static de comprovació prèvia:
A Prestec:
/**
* Indica si es possible registrar un prestec d'aquest material a aquest empleat.
* @return true si el material esta disponible i l'empleat no ha arribat al limit
*/
public static boolean esPotPrestar(Material material, Empleat empleat) {
return material != null && empleat != null
&& material.estaDisponible()
&& empleat.potPrendrePrestat();
}I el constructor canònic ja fa la resta —marca el material, incrementa el comptador de l'empleat i calcula el venciment amb diaPrestec + material.getDiesPrestec()—, així que main queda així:
if (Prestec.esPotPrestar(javaEficac, marta)) {
Prestec p = new Prestec(javaEficac, marta, 100);
System.out.println("Prestec " + p.getReferencia() + " registrat, venc el dia "
+ p.getDiaVenciment());
} else {
System.out.println("No es pot prestar");
}Comparació:
| Abans | Després |
|---|---|
11 línies a main |
1 crida de comprovació + 1 constructor |
El termini de 15 dies escrit a main |
L'aporta el material (i funciona amb revistes i DVD) |
| Tres mutacions manuals que es poden oblidar | Les fa el constructor, sempre |
setDisponible públic |
No existeix |
Si s'afegeix una regla nova, cal buscar-la a tots els main |
S'afegeix a esPotPrestar |
Mètodes nous: només un, Prestec.esPotPrestar(Material, Empleat). Els altres ja existien. I desapareixen tots els setters del fragment original: setDisponible, setPrestecsAcumulats, setMaterial, setEmpleat, setDiaPrestec i setDiaVenciment. Sis portes tancades.
Un detall a discutir: esPotPrestar és static perquè respon una pregunta abans que existeixi el préstec. És una de les poques ocasions en què un mètode static en una classe de domini està justificat.
Conclusió
Has tancat la porta que duies oberta tot el mòdul. Saps que encapsular no és posar accessors, sinó amagar la representació i exposar només operacions que preservin les regles; coneixes els quatre modificadors d'accés i la taula completa de visibilitat, i tens el criteri professional de partir de private i pujar la visibilitat només amb una raó. Saps decidir camp per camp si mereix getter i, sobretot, per què un setter per a cada camp destrueix el que es pretenia protegir: setMulta convertiria la política de multes de Nexus Software en un suggeriment. Has après a formular els invariants d'una classe i a garantir-los tancant totes les portes per les quals l'estat pot canviar. Manegues la immutabilitat —camps final, sense setters, classe final— i entens per què les dades bibliogràfiques d'un Llibre en són el cas ideal. I coneixes la fuita més subtil de totes: retornar una referència mutable interna, amb la còpia defensiva com a remei a l'entrada i a la sortida, i amb el matís que un col·laborador compartit com Material no es copia. Tanques amb encapsulament a nivell de paquet i la Llei de Demeter.
El domini de BiblioTech ja no es pot corrompre des de fora: javaEficac.disponible = true no compila, i els tres invariants d'Empleat, Prestec i Material es mantenen perquè només hi ha uns pocs mètodes que els poden tocar.
Però ha aparegut un problema nou, i l'has vist a getEmpleatNoUsar: la superfície pública de les teves classes ha crescut sense control. Prestec exposa més de vint mètodes, alguns dels quals ningú no hauria de cridar. Encapsular respon a com amagar; falta respondre a què exposar. A la lliçó següent, Abstracció, aprendràs a decidir què entra a l'API pública d'una classe i què no, a no barrejar nivells d'abstracció dins d'un mateix mètode, a documentar contractes amb precondicions i postcondicions, i a reconèixer tant les abstraccions amb fuites com el vici contrari, el d'abstreure el que ningú no ha demanat.
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
