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

  1. El problema: instanciar una classe que no hauria d'existir per si sola
  2. Classes abstractes: la paraula clau abstract
  3. Mètodes abstractes enfront de mètodes virtuals
  4. Convertint MaterialBibliotecari en classe abstracta
  5. MostrarFitxa() continua sent virtual: la diferència a la pràctica
  6. Quan usar una classe abstracta
  7. Abstracció enfront d'interfícies: una distinció conceptual

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

  1. Classes abstractes: la paraula clau abstract

Una 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 abstract

Fixa'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.

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

  1. Convertint MaterialBibliotecari en classe abstracta

Amb 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".

  1. 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 que Llibre i Revista l'enriqueixin afegint la seva dada particular. Per això és virtual: ofereix un comportament per defecte útil, que les hereves poden ampliar si volen.
  • Descriure(), en canvi, no té cap implementació raonable a MaterialBibliotecari: 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ò és abstract: 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.

  1. 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 de Llibre o Revista).
  • 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
Hi ha comportament comú (amb implementació) que diverses hereves han de compartir
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

  1. 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 abstract directament: 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 abstract en una classe hereva: si Llibre no defineix override string Descriure(), el compilador dona un error, a menys que Llibre també es declari com a abstract (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ètode abstract sempre acaba en ;, sense claus { }; si necessites una implementació per defecte (encara que sigui senzilla), el correcte és virtual, no abstract.
  • Marcar una classe com a abstract sense cap mètode abstract: é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 sigui abstract, 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

  1. Declara MaterialBibliotecari com a abstract class, mantenint les seves propietats (Titol, Autor, Disponible amb private set) i el seu constructor. Comprova que new MaterialBibliotecari("...", "...") provoca un error de compilació si ho intentes.

  2. Afegeix a MaterialBibliotecari un mètode public abstract string Descriure();, i proporciona la seva implementació amb override tant a Llibre (incloent l'ISBN) com a Revista (incloent el número d'edició). Crea un objecte de cada classe i mostra el resultat de Descriure().

  3. Afegeix una tercera classe hereva, Dvd : MaterialBibliotecari, amb una propietat pròpia DuracioMinuts (int) i el seu propi constructor amb base(...). Implementa Descriure() perquè retorni, per exemple, "Dvd: <Titol> (<DuracioMinuts> min)". Afegeix-lo a un array MaterialBibliotecari[] junt amb un Llibre i una Revista, i recorre l'array mostrant el resultat de Descriure() 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#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats