La lliçó anterior va acabar assenyalant un detall incòmode: res impedeix escriure new MaterialBibliotecari("Titol qualsevol", "Autor qualsevol") en qualsevol part del programa,
tot i que "un material bibliotecari" a seques, que no sigui ni un llibre ni una revista, no
representa cap objecte real del catàleg de BiblioTech. MaterialBibliotecari sempre es va
pensar com una base comuna, mai com alguna cosa que hagués d'existir per si sola. L'abstracció
és el principi que et permet expressar això directament al codi: declarar que una classe és
purament conceptual, una plantilla per a les seves hereves, i que mai s'ha d'instanciar per si
mateixa. En aquesta lliçó convertiràs MaterialBibliotecari en una classe abstract, i el seu
mètode Descriure() en un mètode abstract que cada hereva està obligada a implementar.
Contingut
- El problema: instanciar una classe que no hauria d'existir per si sola
- Classes abstractes: la paraula clau
abstract - Mètodes abstractes enfront de mètodes virtuals
- Convertint
MaterialBibliotecarien classe abstracta MostrarFitxa()continua sentvirtual: la diferència a la pràctica- Quan usar una classe abstracta
- Abstracció enfront d'interfícies: una distinció conceptual
- El problema: instanciar una classe que no hauria d'existir per si sola
Amb el disseny actual, aquest codi compila i s'executa sense cap error:
MaterialBibliotecari materialGenèric = new MaterialBibliotecari("Sense titol", "Sense autor");
Console.WriteLine(materialGenèric.Descriure()); // "Material: Sense titol, de Sense autor"Res al codi impedeix això, encara que conceptualment no tingui sentit: al domini real de
BiblioTech, tot allò que es presta és sempre, específicament, un llibre o una revista (o, en el
futur, un altre tipus concret), mai "un material" a seques, sense més especificar.
MaterialBibliotecari existeix únicament perquè Llibre i Revista comparteixin codi;
permetre que s'instanciï directament és una possibilitat que el disseny hauria de tancar
explícitament, no deixar oberta per descuit.
- Classes abstractes: la paraula clau
abstract
abstractUna classe declarada amb abstract no es pot instanciar directament amb new; només es
pot fer servir com a classe base d'altres classes (no abstractes) que n'heretin:
abstract 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;
Console.WriteLine($"'{Titol}' ha estat prestat.");
}
else
{
Console.WriteLine($"'{Titol}' no esta disponible per a prestec.");
}
}
public void Retornar()
{
Disponible = true;
Console.WriteLine($"'{Titol}' ha estat retornat.");
}
}// MaterialBibliotecari materialGenèric = new MaterialBibliotecari("...", "...");
// Error de compilacio: no es pot crear una instancia de la classe abstracta 'MaterialBibliotecari'
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"); // Ok: Llibre no es abstractFixa't en un detall important: encara que MaterialBibliotecari ja no es pugui instanciar
directament, continua tenint un constructor, i aquest constructor continua executant-se
—a través de base(titol, autor)— cada vegada que es crea un Llibre o una Revista. Una
classe abstracta pot tenir constructors, camps, propietats i mètodes amb implementació
completa, exactament igual que una classe normal; l'única cosa que canvia és que ella
mateixa no es pot convertir en un objecte independent.
- Mètodes abstractes enfront de mètodes virtuals
A més de marcar la classe sencera com a abstract, es pot marcar un mètode com a
abstract. Un mètode abstracte declara la seva signatura (nom, paràmetres, tipus de retorn)
però no té cos: no hi ha cap implementació a la classe base, i cada classe hereva està
obligada a proporcionar la seva pròpia amb override.
abstract class MaterialBibliotecari
{
// ... propietats, constructor, Prestar(), Retornar() com abans ...
public abstract string Descriure(); // sense cos, acaba amb ";"
}Això és diferent de virtual, que sí que exigeix (o ofereix) una implementació per defecte a
la classe base, i deixa la sobreescriptura com a opcional per a les hereves.
virtual |
abstract |
|
|---|---|---|
| Implementació a la classe base | Obligatòria (té cos) | Prohibida (només la signatura, acaba en ;) |
| Sobreescriure a la classe hereva | Opcional | Obligatori |
| Es pot fer servir en una classe no abstracta | Sí | No; una classe amb algun membre abstract ha de ser, ella mateixa, abstract |
| Exemple a BiblioTech | MostrarFitxa() |
Descriure() |
Si Llibre o Revista no proporcionessin la seva pròpia implementació de Descriure(), el
compilador donaria un error: en ser abstract, no existeix cap versió "per defecte" a la qual
recórrer.
- Convertint
MaterialBibliotecari en classe abstracta
MaterialBibliotecari en classe abstractaAmb aquests dos canvis (la classe abstract i Descriure() com a mètode abstract), així
queda el model complet de la jerarquia de materials de BiblioTech:
abstract 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;
Console.WriteLine($"'{Titol}' ha estat prestat.");
}
else
{
Console.WriteLine($"'{Titol}' no esta disponible per a prestec.");
}
}
public void Retornar()
{
Disponible = true;
Console.WriteLine($"'{Titol}' ha estat retornat.");
}
public virtual void MostrarFitxa()
{
Console.WriteLine($"Titol: {Titol}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
public abstract string Descriure();
}
class Llibre : MaterialBibliotecari
{
public string Isbn { get; set; }
public Llibre(string titol, string autor, string isbn) : base(titol, autor)
{
Isbn = isbn;
}
public override string Descriure()
{
return $"Llibre: {Titol}, de {Autor} (ISBN {Isbn})";
}
}
class Revista : MaterialBibliotecari
{
public int NumeroEdicio { get; set; }
public Revista(string titol, string autor, int numeroEdicio) : base(titol, autor)
{
NumeroEdicio = numeroEdicio;
}
public override string Descriure()
{
return $"Revista: {Titol}, edicio numero {NumeroEdicio}";
}
}La resta del codi que ja vas escriure a la lliçó de Polimorfisme —l'array
MaterialBibliotecari[] recorregut amb foreach, is, as— continua funcionant exactament
igual: l'abstracció no canvia com s'utilitzen els objectes Llibre i Revista, només
impedeix que es creï un MaterialBibliotecari "a seques".
MostrarFitxa() continua sent virtual: la diferència a la pràctica
MostrarFitxa() continua sent virtual: la diferència a la pràcticaÉs intencionat que Descriure() sigui abstract però MostrarFitxa() continuï sent només
virtual: són dues situacions diferents que il·lustren quan usar cadascuna.
MostrarFitxa()té sentit amb una implementació genèrica raonable (mostrar títol, autor i disponibilitat) que serveix tal qual per a qualsevol material, encara queLlibreiRevistal'enriqueixin afegint la seva dada particular. Per això ésvirtual: ofereix un comportament per defecte útil, que les hereves poden ampliar si volen.Descriure(), en canvi, no té cap implementació raonable aMaterialBibliotecari: no hi ha cap frase genèrica que tingui sentit per a "qualsevol material bibliotecari" sense saber si és un llibre o una revista. Per això ésabstract: obliga que cada tipus concret decideixi com descriure's, sense oferir (ni permetre) una versió per defecte que no encaixaria bé en cap cas real.
Aquesta distinció —existeix una implementació per defecte raonable, o no n'hi ha?— és la
pregunta clau per decidir si un membre ha de ser virtual o abstract.
- Quan usar una classe abstracta
Convé declarar una classe com a abstract quan es compleixen, alhora, aquestes condicions:
- La classe representa un concepte general que només té sentit a través de les seves
especialitzacions concretes (com
MaterialBibliotecari, enfront deLlibreoRevista). - Vols compartir codi comú (propietats, mètodes amb implementació) entre diverses classes relacionades, evitant duplicació —el mateix motiu que ja vas veure a la lliçó d'Herència.
- Vols forçar, a nivell de compilador, que certs mètodes s'implementin a cada hereva concreta, sense arriscar-te que algú ho oblidi.
| Senyal | Classe abstracta? |
|---|---|
| La classe mai s'hauria d'instanciar per si sola, només les seves hereves | Sí |
| Hi ha comportament comú (amb implementació) que diverses hereves han de compartir | Sí |
| Alguns mètodes no tenen una implementació raonable sense saber el tipus concret | Sí, aquests mètodes com a abstract |
| Totes les instàncies possibles d'aquest concepte són, a la pràctica, del mateix tipus exacte (no hi ha variants) | No; una classe normal n'hi ha prou |
- Abstracció enfront d'interfícies: una distinció conceptual
C# ofereix una altra eina, molt relacionada, per expressar abstracció: les interfícies
(interface). La diferència conceptual essencial és aquesta: una classe abstracta modela una
relació de tipus "és un" amb herència de codi compartit (Llibre és un
MaterialBibliotecari, i hereta el seu Prestar()/Retornar() ja implementats), mentre que
una interfície modela un contracte de "pot fer" sense cap implementació compartida ni
relació d'herència d'una única classe base. A més, una classe en C# només pot heretar d'una
classe abstracta (recorda l'herència simple del Mòdul 3), però pot implementar diverses
interfícies alhora.
En aquest mòdul no aprofundirem més en les interfícies: s'estudien en detall, amb tota la seva
sintaxi i els seus casos d'ús, al Mòdul 4: Conceptes Avançats de C#, just a la primera
lliçó. Per ara, queda't amb la idea que abstract class i interface són dues eines diferents
per expressar abstracció, i que BiblioTech farà servir totes dues més endavant, cadascuna on
tingui més sentit.
Errors Comuns i Consells
- Intentar instanciar una classe
abstractdirectament:new MaterialBibliotecari(...)no compila mai, sigui quin sigui el constructor que tingui definit; només es poden crear instàncies de les seves classes hereves no abstractes (Llibre,Revista). - Oblidar implementar un mètode
abstracten una classe hereva: siLlibreno defineixoverride string Descriure(), el compilador dona un error, a menys queLlibretambé es declari com aabstract(la qual cosa traslladaria l'obligació a la seva pròpia hereva, en lloc de resoldre-la). - Posar cos a un mètode
abstract: un mètodeabstractsempre acaba en;, sense claus{ }; si necessites una implementació per defecte (encara que sigui senzilla), el correcte ésvirtual, noabstract. - Marcar una classe com a
abstractsense cap mètodeabstract: és vàlid (simplement impedeix instanciar-la directament), però si no hi ha cap membre que obligui a implementar alguna cosa diferent a cada hereva, convé preguntar-se si de veritat cal que siguiabstract, o si n'hi hauria prou amb una classe normal que simplement no s'instancia a la pràctica. - Confondre classe abstracta amb interfície: si necessites compartir implementació real entre classes relacionades per herència, fes servir una classe abstracta; si només necessites garantir que diversos tipus no relacionats entre si ofereixin certs mètodes, sense compartir codi, una interfície (Mòdul 4) sol encaixar millor.
Exercicis
-
Declara
MaterialBibliotecaricom aabstract class, mantenint les seves propietats (Titol,Autor,Disponibleambprivate set) i el seu constructor. Comprova quenew MaterialBibliotecari("...", "...")provoca un error de compilació si ho intentes. -
Afegeix a
MaterialBibliotecariun mètodepublic abstract string Descriure();, i proporciona la seva implementació amboverridetant aLlibre(incloent l'ISBN) com aRevista(incloent el número d'edició). Crea un objecte de cada classe i mostra el resultat deDescriure(). -
Afegeix una tercera classe hereva,
Dvd : MaterialBibliotecari, amb una propietat pròpiaDuracioMinuts(int) i el seu propi constructor ambbase(...). ImplementaDescriure()perquè retorni, per exemple,"Dvd: <Titol> (<DuracioMinuts> min)". Afegeix-lo a un arrayMaterialBibliotecari[]junt amb unLlibrei unaRevista, i recorre l'array mostrant el resultat deDescriure()de cadascun.
Solucions
abstract 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;
}
}
// MaterialBibliotecari m = new MaterialBibliotecari("X", "Y");
// Error de compilacio: no es pot crear una instancia de la classe abstracta
public abstract string Descriure();
// A Llibre:
public override string Descriure()
{
return $"Llibre: {Titol}, de {Autor} (ISBN {Isbn})";
}
// A Revista:
public override string Descriure()
{
return $"Revista: {Titol}, edicio numero {NumeroEdicio}";
}
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Revista revista1 = new Revista("National Geographic", "Diversos autors", 302);
Console.WriteLine(llibre1.Descriure());
Console.WriteLine(revista1.Descriure());
class Dvd : MaterialBibliotecari
{
public int DuracioMinuts { get; set; }
public Dvd(string titol, string autor, int duracioMinuts) : base(titol, autor)
{
DuracioMinuts = duracioMinuts;
}
public override string Descriure()
{
return $"Dvd: {Titol} ({DuracioMinuts} min)";
}
}
MaterialBibliotecari[] cataleg = new MaterialBibliotecari[]
{
new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
new Revista("National Geographic", "Diversos autors", 302),
new Dvd("El Padrino", "Francis Ford Coppola", 175)
};
foreach (MaterialBibliotecari material in cataleg)
{
Console.WriteLine(material.Descriure());
}
Aquest exercici comprova, a la pràctica, l'avantatge del polimorfisme combinat amb
abstracció: el bucle foreach no canvia en absolut en afegir Dvd com a tercer tipus de
material, i cadascun es descriu correctament segons la seva pròpia implementació.
Conclusió
En aquesta lliçó has acabat de tancar el disseny de la jerarquia de materials de BiblioTech:
MaterialBibliotecari és ara una classe abstract que no es pot instanciar directament, amb
Descriure() com a mètode abstract que obliga cada hereva concreta (Llibre, Revista) a
definir la seva pròpia descripció, mentre que MostrarFitxa() es manté virtual per tenir una
implementació per defecte raonable. També has vist la diferència conceptual entre una classe
abstracta i una interfície, que estudiaràs en detall al Mòdul 4.
Amb això es completa el disseny orientat a objectes del model de domini de BiblioTech:
MaterialBibliotecari (abstracta) amb Llibre i Revista com a hereves concretes, Soci i
Prestec completant la resta. Queda una última peça per resoldre en aquest mòdul: fins ara,
tot el que has modelat són classes, és a dir, tipus per referència. A la propera lliçó, la
darrera del mòdul, coneixeràs els structs i els records, alternatives més lleugeres per
a dades petites i immutables (com una data de préstec o l'adreça d'un soci), i entendràs quan
convé triar cadascuna enfront d'una classe tradicional.
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
