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

  1. Què és realment encapsular
  2. Els quatre modificadors d'accés
  3. Taula completa de visibilitat
  4. Getters i setters: el que la recepta mecànica no explica
  5. Invariants de classe
  6. Operacions de negoci en lloc de setters
  7. Immutabilitat
  8. El perill de retornar referències mutables: còpia defensiva
  9. Encapsulament a nivell de paquet
  10. La Llei de Demeter
  11. BiblioTech: el domini encapsulat
  12. Errors Habituals i Consells
  13. Exercicis

  1. 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:

public double multa;      // public: vint fitxers el llegeixen

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ó.

  1. 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).

  1. 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 No No No
(default) No No
protected No
public

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 private per 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.

  1. 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 constructor

Un 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:

  1. Algú de fora necessita llegir aquesta dada? Si no, no hi ha getter.
  2. 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 No El títol d'un llibre no canvia
Llibre.isbn No És la seva identitat
Material.disponible Sí, com a estaDisponible() No Canvia només mitjançant prestar() / retornar()
Empleat.nom No Dada identitària
Empleat.prestecsAcumulats No Canvia mitjançant registrarPrestec() / registrarDevolucio()
Prestec.diesTranscorreguts 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 dies

Tota 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.

  1. 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.

  1. 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:

prestec.registrarDevolucio(dies);

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 No
Es pot falsejar la multa 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.

  1. Immutabilitat

Un objecte immutable és aquell l'estat del qual no pot canviar després de construït. S'aconsegueix amb quatre regles:

  1. Tots els camps private final.
  2. Cap setter ni mètode que modifiqui l'estat.
  3. La classe declarada final, perquè cap subclasse no hi afegeixi mutabilitat.
  4. 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 camps private final, el constructor, els accessors, i a més equals, hashCode i toString. S'estudien a la lliçó 04-07. Escriu primer la versió manual: entendre què genera el record é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.

  1. 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 prestec

final 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.

  1. 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'us

El 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.

  1. 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(...) o sb.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.

  1. 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'Empleat sencer permet a qualsevol fer prestec.getEmpleat().registrarPrestec() i descompensar el sistema. Per això el préstec ofereix getNomEmpleat() i empleatAlLimit() 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'ignora

Sortida:

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. final no protegeix l'objecte apuntat.
  • Fer servir protected als 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ètode protected.
  • Guardar dades derivades. Si multa es pot calcular a partir dels dies, calcula-la. Una dada duplicada és una dada que es pot desincronitzar. (A Prestec guardem multaAplicada nomé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 public una 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 final avui. É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:

  1. Fes tots els camps private, i final els que no hagin de canviar.
  2. Decideix, camp per camp, si necessita getter, justificant-ho en un comentari.
  3. Substitueix qualsevol setter per operacions de negoci: cancellar(), ampliarUnaHora() i canviarAssistents(int) (que ha de rebutjar valors fora de rang).
  4. 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'h

Qui crida conserva una referència al mateix array que ara és estat intern de l'objecte.

Fuita 2: el getter retorna l'array intern.

h.getReferenciesPrestecs()[1] = "UNA ALTRA DE FALSA";   // s'escriu dins d'h

Ni private ni final protegeixen: final impedeix reassignar la variable, no modificar l'array.

Fuita 3: setTotalMultes permet qualsevol valor.

h.setTotalMultes(-500);   // total de multes negatiu: estat impossible

É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());                           // 0

Un 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

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats