Hi ha un problema latent al model de BiblioTech tal com ha quedat fins ara: qualsevol part del
programa pot escriure directament llibre1.Disponible = true;, sense passar per Prestar()
ni Retornar(), deixant el catàleg en un estat incoherent (per exemple, un llibre marcat
disponible que en realitat continua en mans d'un soci). L'encapsulació és el principi de
la programació orientada a objectes que resol precisament això: ocultar l'estat intern d'un
objecte i exposar únicament les operacions controlades a través de les quals aquest estat pot
canviar. En aquesta lliçó aprendràs els modificadors d'accés de C# i tancaràs aquesta porta
del darrere de Disponible, perquè només es pugui modificar mitjançant els mètodes pensats
per a això.
Contingut
- Què és l'encapsulació i per què importa
- Modificadors d'accés:
public,private,protected,internal - El problema concret:
Disponiblemodificable lliurement private set: exposar lectura, restringir escriptura- Propietats amb lògica de validació al
set - Propietats de només lectura
- Aplicant encapsulació a la resta del model:
SociiPrestec - El principi de mínima exposició
- Què és l'encapsulació i per què importa
L'encapsulació consisteix a ocultar els detalls interns de com un objecte emmagatzema i
gestiona el seu estat, exposant cap enfora únicament una interfície controlada d'operacions
vàlides. La idea central és: el codi extern a la classe no hauria de poder deixar un objecte
en un estat incoherent. Fins ara, qualsevol codi amb accés a un objecte Llibre pot escriure
llibre1.Disponible = false; sense haver cridat realment Prestar(), saltant-se per complet
la lògica que decideix si el préstec és vàlid (per exemple, comprovar que el llibre estigués
realment disponible abans de prestar-lo). L'encapsulació tanca aquesta via d'escapament.
- Modificadors d'accés:
public, private, protected, internal
public, private, protected, internalC# ofereix diversos modificadors d'accés que controlen des d'on es pot fer servir un membre (propietat, mètode, camp) d'una classe:
| Modificador | Accessible des de... |
|---|---|
public |
Qualsevol part del programa, sense restriccions |
private |
Únicament des de dins de la pròpia classe |
protected |
Des de la pròpia classe i des de qualsevol classe que n'heretés |
internal |
Des de qualsevol part del mateix assemblatge (projecte compilat), però no des d'altres projectes externs |
Un exemple breu i genèric, fora del model de BiblioTech, ajuda a fixar la diferència entre
private i protected:
class Base
{
private int valorPrivat = 1;
protected int valorProtegit = 2;
public int valorPublic = 3;
public void MostrarDesDeBase()
{
Console.WriteLine(valorPrivat); // Ok: mateixa classe
Console.WriteLine(valorProtegit); // Ok: mateixa classe
}
}
class Derivada : Base
{
public void MostrarDesDeDerivada()
{
// Console.WriteLine(valorPrivat); // Error: no accessible, es private a Base
Console.WriteLine(valorProtegit); // Ok: protected SI es accessible des d'una classe hereva
Console.WriteLine(valorPublic); // Ok: public, accessible des de qualsevol lloc
}
}
Derivada objecte = new Derivada();
Console.WriteLine(objecte.valorPublic); // Ok: public
// Console.WriteLine(objecte.valorProtegit); // Error: protected no es accessible des de fora de la jerarquiaprotected ocupa una posició intermèdia entre private (només la pròpia classe) i public
(tot el programa): és visible per a les classes hereves, però continua ocult per a la resta de
codi extern a la jerarquia. Aquest curs farà servir sobretot public i private; protected
apareixerà de forma puntual quan una classe base necessiti compartir alguna cosa amb les seves
hereves sense exposar-ho a la resta del programa, i internal no farà falta als exemples de
BiblioTech, ja que tot el projecte es compila com una única aplicació.
- El problema concret:
Disponible modificable lliurement
Disponible modificable lliurementAixí estava definida, fins ara, la propietat Disponible:
class MaterialBibliotecari
{
public string Titol { get; set; }
public string Autor { get; set; }
public bool Disponible { get; set; } = true;
// ...
}Amb { get; set; } públics, res impedeix aquest codi, en qualsevol part del programa:
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
llibre1.Disponible = true; // Es salta per complet la logica de Prestar()/Retornar()Aquest Disponible = true directe no passa per cap comprovació: no verifica si hi havia un
préstec en curs, no registra cap missatge, no aplica cap regla de negoci. El resultat és un
objecte l'estat del qual (Disponible) pot quedar desincronitzat de la resta del sistema (per
exemple, dels objectes Prestec que referencien aquell llibre).
private set: exposar lectura, restringir escriptura
private set: exposar lectura, restringir escripturaLa solució més directa és permetre que Disponible es llegeixi des de qualsevol part
(public get), però que només s'escrigui des de dins de la pròpia classe (private set):
class MaterialBibliotecari
{
public string Titol { get; set; }
public string Autor { get; set; }
public bool Disponible { get; private set; } = true;
public MaterialBibliotecari(string titol, string autor)
{
Titol = titol;
Autor = autor;
}
public void Prestar()
{
if (Disponible)
{
Disponible = false; // Ok: estem dins de la propia classe
Console.WriteLine($"'{Titol}' ha estat prestat.");
}
else
{
Console.WriteLine($"'{Titol}' no esta disponible per a prestec.");
}
}
public void Retornar()
{
Disponible = true; // Ok: estem dins de la propia classe
Console.WriteLine($"'{Titol}' ha estat retornat.");
}
}Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(llibre1.Disponible); // Ok: lectura publica, True
llibre1.Prestar(); // Ok: canvia Disponible internament, a traves del metode
// llibre1.Disponible = true; // Error de compilacio: el set es private, no accessible des de foraAmb private set, l'última línia ja no compila: el compilador impedeix, ara de forma
garantida (no només per convenció o bona voluntat), que codi extern a la classe assigni un
valor a Disponible sense passar per Prestar() o Retornar(). Aquesta és la forma més
simple i habitual d'encapsular una propietat en C#: exposar la lectura, tancar l'escriptura
dins d'operacions amb significat (Prestar, Retornar), en lloc d'un set genèric.
- Propietats amb lògica de validació al
set
setprivate set resol el cas de Disponible, però de vegades cal alguna cosa més flexible:
permetre l'escriptura des de fora, però validant el valor abans d'acceptar-lo. Per a això
cal una propietat completa, amb un camp (backing field) explícit al darrere:
class MaterialBibliotecari
{
private string _titol;
public string Titol
{
get { return _titol; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("El titol no pot estar buit.");
}
_titol = value;
}
}
// Autor, Disponible, constructor, Prestar(), Retornar() com a l'apartat anterior
}Dins del bloc set, la paraula clau value representa automàticament el valor que s'està
intentant assignar. Aquí, si algú intenta posar un títol buit o compost només d'espais
(string.IsNullOrWhiteSpace), la propietat llança una ArgumentException —recorda el maneig
d'excepcions del Mòdul 2— en lloc d'acceptar una dada no vàlida. _titol, amb guió baix
inicial en minúscula, és la convenció habitual en C# per anomenar el camp privat que dona
suport a una propietat amb lògica pròpia.
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
llibre1.Titol = "";
// Llanca ArgumentException: "El titol no pot estar buit."Propietat autoimplementada ({ get; set; }) |
Propietat completa amb validació | |
|---|---|---|
| Sintaxi | Una sola línia, sense camp visible | get/set explícits, amb camp _titol propi |
| Permet validar el valor abans d'acceptar-lo | No | Sí |
| Quan usar-la | La dada no necessita cap restricció especial | Hi ha una regla de negoci que s'ha de complir sempre |
- Propietats de només lectura
Un tercer nivell de restricció, més estricte que private set, és no declarar cap set en
absolut: la propietat només es pot assignar dins del constructor (o mitjançant un
inicialitzador), i mai més després:
class Soci
{
public int Id { get; } // nomes lectura: mai canvia despres de crear l'objecte
public string Nom { get; set; }
public Soci(int id, string nom)
{
Id = id;
Nom = nom;
}
}public int Id { get; } (sense set de cap tipus) expressa la intenció amb total claredat:
l'identificador d'un soci es fixa en crear-lo i no ha de canviar mai durant tota la vida de
l'objecte. Intentar escriure soci1.Id = 5; fora del constructor seria un error de
compilació, igual que passaria amb private set, però aquí queda clar que ni tan sols la
pròpia classe necessita reassignar-lo més endavant.
- Aplicant encapsulació a la resta del model:
Soci i Prestec
Soci i PrestecAmb aquestes tres tècniques (private set, propietats amb validació, i propietats de només
lectura), es pot revisar la resta del model de BiblioTech perquè cada dada s'exposi únicament
amb el nivell de control que li correspon:
class Prestec
{
public Llibre Llibre { get; }
public Soci Soci { get; }
public DateTime DataPrestec { get; }
public DateTime? DataDevolucio { get; private set; }
public Prestec(Llibre llibre, Soci soci)
{
Llibre = llibre;
Soci = soci;
DataPrestec = DateTime.Now;
DataDevolucio = null;
}
public void RegistrarDevolucio()
{
DataDevolucio = DateTime.Now;
}
}Ara Llibre, Soci i DataPrestec d'un Prestec són de només lectura (no té sentit que un
préstec "canviï" de llibre o de soci una vegada creat), i DataDevolucio només es pot fixar
des de dins de la pròpia classe Prestec, a través del mètode RegistrarDevolucio() —mai
assignant la data directament des de fora:
Llibre llibre1 = new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1");
Soci soci1 = new Soci(1, "Ana Martinez");
llibre1.Prestar();
Prestec prestec1 = new Prestec(llibre1, soci1);
// prestec1.DataDevolucio = DateTime.Now; // Error de compilacio: el set es private
prestec1.RegistrarDevolucio(); // forma correcta i unica de registrar la devolucio
llibre1.Retornar();
- El principi de mínima exposició
Tot el que s'ha vist en aquesta lliçó es resumeix en un únic principi pràctic: exposa de la
teva classe únicament el mínim que la resta del programa necessita fer servir, i res més.
Cada propietat, mètode o camp públic és una promesa cap a la resta del codi: com més
n'exposis, més formes tindrà la resta del programa d'acoblar-se als detalls interns de la teva
classe, i més difícil serà canviar aquests detalls demà sense trencar alguna cosa. Abans de
marcar alguna cosa com a public, pregunta't si de veritat ho ha de ser, o si n'hi hauria
prou amb private (o, en el cas de propietats, amb restringir només el set).
| Regla pràctica | Aplicada a BiblioTech |
|---|---|
| Si una dada no ha de canviar després de crear-se, fes-la de només lectura | Soci.Id, Prestec.Llibre, Prestec.Soci, Prestec.DataPrestec |
Si una dada només ha de canviar mitjançant una operació concreta, restringeix el set |
MaterialBibliotecari.Disponible (només via Prestar()/Retornar()), Prestec.DataDevolucio (només via RegistrarDevolucio()) |
| Si una dada necessita complir una regla en assignar-se, fes servir una propietat completa amb validació | MaterialBibliotecari.Titol |
Si una dada pot canviar lliurement sense cap restricció, un { get; set; } públic és suficient |
Soci.Nom |
Errors Comuns i Consells
- Deixar totes les propietats com a
{ get; set; }públic "per si de cas": és l'opció més còmoda a curt termini, però renuncia a tota la protecció que ofereix l'encapsulació; revisa, propietat a propietat, si de veritat necessita poder escriure's lliurement des de fora. - Oblidar inicialitzar un camp
privateamb propietat completa: si defineixes_titolsense cap valor per defecte ni assignació al constructor, la propietatTitolretornarianullfins a la primera assignació vàlida; assegura't que el constructor passi sempre pelset(assignantTitol = titol;, no_titol = titol;directament, perquè la validació també s'apliqui a la construcció). - Confondre
private setambprivatea seques:private setcontinua permetent la lectura pública (getcontinua sentpublic); el que es restringeix és només l'escriptura. Un camp completamentprivateno seria accessible ni per llegir des de fora de la classe. - Fer servir
protectedcom a solució per defecte:protectedexposa el membre a totes les classes hereves presents i futures, la qual cosa també és una forma d'acoblament; reserva'l per a quan de veritat una classe base necessiti compartir alguna cosa amb les seves hereves. - Consell: en dissenyar una classe nova, comença marcant-ho tot com a
privatei vés obrint únicament allò que la resta del programa necessiti de veritat, en lloc de començar amb totpublici restringir després; és molt més fàcil obrir accés més endavant que tancar-lo sense trencar codi que ja en depenia.
Exercicis
-
Modifica la propietat
DisponibledeMaterialBibliotecariperquè tingui ungetpúblic i unsetprivat (private set). Comprova que, després de crear un objecteLlibre, la líniallibre1.Disponible = true;escrita fora de la classe provoca un error de compilació, mentre quellibre1.Prestar()continua funcionant amb normalitat. -
Converteix la propietat
TitoldeMaterialBibliotecarien una propietat completa amb un camp privat_titol, elsetdel qual llanci unaArgumentExceptionamb el missatge"El titol no pot estar buit."si el valor rebut és una cadena buida o només espais (fes servirstring.IsNullOrWhiteSpace). Prova a assignar un títol buit i comprova que es llança l'excepció. -
A la classe
Prestec, declaraDataDevoluciocom aDateTime?ambgetpúblic isetprivat, i afegeix un mètodevoid RegistrarDevolucio()que li assigniDateTime.Now. Crea unPrestec, comprova queDataDevoluciocomença ennull, i després de cridarRegistrarDevolucio(), comprova que ja té un valor.
Solucions
class MaterialBibliotecari
{
public string Titol { get; set; }
public string Autor { get; set; }
public bool Disponible { get; private set; } = true;
public MaterialBibliotecari(string titol, string autor)
{
Titol = titol;
Autor = autor;
}
public void Prestar()
{
if (Disponible)
{
Disponible = false;
}
}
}
// llibre1.Disponible = true; // Error de compilacio: el set es private
private string _titol;
public string Titol
{
get { return _titol; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("El titol no pot estar buit.");
}
_titol = value;
}
}
En executar llibre1.Titol = "";, es llança ArgumentException amb el missatge "El titol no pot estar buit.", ja que la cadena buida compleix la condició de
string.IsNullOrWhiteSpace.
class Prestec
{
public Llibre Llibre { get; }
public Soci Soci { get; }
public DateTime DataPrestec { get; }
public DateTime? DataDevolucio { get; private set; }
public Prestec(Llibre llibre, Soci soci)
{
Llibre = llibre;
Soci = soci;
DataPrestec = DateTime.Now;
DataDevolucio = null;
}
public void RegistrarDevolucio()
{
DataDevolucio = DateTime.Now;
}
}
Prestec prestec1 = new Prestec(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"), new Soci(1, "Ana Martinez"));
Console.WriteLine(prestec1.DataDevolucio); // (buit / null)
prestec1.RegistrarDevolucio();
Console.WriteLine(prestec1.DataDevolucio); // data i hora actuals
Conclusió
En aquesta lliçó has protegit l'estat intern del model de BiblioTech: els modificadors
d'accés (public, private, protected, internal), la restricció d'escriptura amb
private set, les propietats completes amb validació al set, i les propietats de només
lectura, aplicades a MaterialBibliotecari.Disponible, MaterialBibliotecari.Titol i
Prestec.DataDevolucio. Ja no és possible deixar un llibre o un préstec en un estat
incoherent saltant-se la lògica de negoci: tota modificació passa, obligatòriament, pels
mètodes pensats per a això.
Queda encara un cap solt conceptual: MaterialBibliotecari es pot instanciar directament amb
new MaterialBibliotecari("...", "..."), encara que a la pràctica no representa cap material
real de BiblioTech (només té sentit com a base de Llibre o Revista). A la propera lliçó,
Abstracció, aprendràs a impedir aquesta instanciació directa convertint
MaterialBibliotecari en una classe abstract, deixant clar en el propi codi que el seu únic
propòsit és servir de base a altres classes.
Curs de Programació en C#
Mòdul 1: Introducció al C#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
