El Mòdul 3 va acabar amb una promesa pendent: a la lliçó d'Abstracció vas veure que
abstract class i interface són "dues eines diferents per expressar abstracció", i que les
interfícies s'estudiarien en detall en aquest mòdul. Ha arribat el moment. Una interfície
és un contracte: una llista de membres (mètodes, propietats, esdeveniments) que una classe es
compromet a implementar, sense que la interfície mateixa aporti cap codi. Aquesta lliçó obre el
Mòdul 4 de Conceptes Avançats definint dues interfícies per a BiblioTech —IPrestable i
ICercable— i aplicant-les sobre el model de domini que ja coneixes, com a primer pas cap a la
resta d'eines modernes de C# que veuràs en aquest mòdul.
Contingut
- Què és una interfície i quin problema resol
- Interfícies enfront de classes abstractes: recuperant la distinció del Mòdul 3
- Definir i implementar una interfície:
IPrestable - Implementació múltiple d'interfícies
- Una segona interfície:
ICercable - Membres amb implementació per defecte (C# 8 en endavant)
- El model de BiblioTech amb interfícies aplicades
- Què és una interfície i quin problema resol
Una interfície declara què ha de poder fer un tipus, sense dir com ho fa. Es defineix
amb la paraula clau interface, per convenció amb el prefix I (IPrestable, ICercable, o
les ja conegudes IEnumerable, IComparable de la mateixa biblioteca de .NET), i enumera
membres sense cap cos d'implementació:
Qualsevol classe que declari : IPrestable es compromet, davant el compilador, a oferir una
propietat Disponible (almenys amb lectura), i els mètodes Prestar() i Retornar() amb
exactament aquestes signatures. Si en falta algun, el codi no compila. La interfície en si
mateixa no es pot instanciar (new IPrestable() no té sentit i no compila) ni conté cap camp
amb estat propi: és pura forma, cap contingut.
El problema que resol és el de desacoblar el contracte de la implementació: codi que només
necessita saber que alguna cosa "es pot prestar" pot treballar amb la interfície IPrestable
sense importar-li si per sota hi ha un Llibre, una Revista o qualsevol tipus futur que ni
tan sols existeixi encara, sempre que compleixi el contracte.
- Interfícies enfront de classes abstractes: recuperant la distinció del Mòdul 3
A la lliçó d'Abstracció vas veure que una classe abstracta modela una relació "és un" amb herència de codi compartit, mentre que una interfície modela un contracte de "pot fer" sense cap implementació compartida. Amb les interfícies ja definides formalment, aquesta taula recull la comparació completa:
Classe abstracta (abstract class) |
Interfície (interface) |
|
|---|---|---|
| Relació que modela | "És un" (Llibre és un MaterialBibliotecari) |
"Pot fer" (Llibre es pot prestar) |
| Implementació compartida | Sí, en els membres no abstract |
No, tret de membres per defecte (apartat 6) |
| Constructor propi | Sí | No |
| Camps amb estat | Sí | No |
| Quantes en pot combinar una classe | Una única classe base | Diverses interfícies alhora |
| Exemple a BiblioTech | MaterialBibliotecari |
IPrestable, ICercable |
La regla pràctica per decidir entre totes dues: si necessites compartir codi real (propietats ja
implementades, mètodes amb cos) entre classes relacionades per herència, fes servir una classe
abstracta; si només necessites garantir que diversos tipus —relacionats entre si o no— ofereixin
certes capacitats, sense compartir cap implementació, fes servir una interfície. De fet, totes
dues eines no són excloents: en el següent apartat veuràs que MaterialBibliotecari continua
sent abstract class i, a més, implementa interfícies.
- Definir i implementar una interfície:
IPrestable
IPrestableImplementar una interfície consisteix a declarar-la després del nom de la classe (després de la
classe base, si n'hi ha, separades per comes) i assegurar-se que tots els seus membres estan
coberts per membres public amb la mateixa signatura:
abstract class MaterialBibliotecari : IPrestable
{
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();
}Fixa't en un detall important: MaterialBibliotecari ja tenia Disponible, Prestar() i
Retornar() des del Mòdul 3, amb exactament l'aspecte que IPrestable exigeix. No va caldre
escriure ni una línia de codi nova per "complir" la interfície: n'hi va haver prou d'afegir
: IPrestable i el compilador comprova que els membres ja existents encaixen amb el contracte.
Això és habitual: les interfícies solen formalitzar capacitats que una classe ja tenia, donant-los
un nom explícit i reutilitzable.
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = llibre1; // valid: Llibre es-un MaterialBibliotecari, que es-un IPrestable
prestable.Prestar();
Console.WriteLine(prestable.Disponible); // FalseUna variable de tipus interfície (IPrestable prestable) només pot fer servir els membres
declarats a la interfície (Disponible, Prestar(), Retornar()), encara que l'objecte
subjacent sigui un Llibre amb molts més membres propis (Isbn, Descriure()...); per accedir
a aquests altres membres caldria convertir de nou a Llibre, amb is/as, com ja vas veure a
la lliçó de Polimorfisme.
- Implementació múltiple d'interfícies
A diferència de l'herència de classes (una classe només pot tenir una classe base directa), una classe pot implementar diverses interfícies alhora, separades per comes:
class Exemple : ClasseBase, IPrestable, ICercable, IComparable<Exemple>
{
// ha de complir el contracte de les tres interficies, a mes d'heretar de ClasseBase
}Aquest és un dels avantatges pràctics més citats de les interfícies enfront de les classes
abstractes: permeten combinar capacitats independents entre si (una cosa pot ser, alhora,
"prestable", "cercable" i "comparable") sense forçar una única jerarquia d'herència rígida. En
el següent apartat, MaterialBibliotecari passarà a implementar dues interfícies diferents
alhora.
- Una segona interfície:
ICercable
ICercablePer completar l'exemple, es defineix una segona interfície pensada per a les cerques al catàleg de BiblioTech (una capacitat que faràs servir de forma més completa a la lliçó de LINQ, més endavant en aquest mòdul):
MaterialBibliotecari implementa totes dues interfícies alhora, i declara Coincideix() com a
virtual (no forma part del contracte de la interfície que un mètode hagi de ser virtual, però
res ho impedeix: és una decisió de disseny perquè Llibre i Revista puguin enriquir la cerca
amb les seves pròpies dades):
abstract class MaterialBibliotecari : IPrestable, ICercable
{
// ... Titol, Autor, Disponible, constructor, Prestar(), Retornar(), MostrarFitxa() ...
public abstract string Descriure();
public virtual bool Coincideix(string text)
{
return Titol.Contains(text, StringComparison.OrdinalIgnoreCase)
|| Autor.Contains(text, StringComparison.OrdinalIgnoreCase);
}
}Llibre sobreescriu Coincideix() perquè la cerca també tingui en compte l'ISBN, reutilitzant
la implementació de la classe base amb base.Coincideix(text):
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})";
}
public override bool Coincideix(string text)
{
return base.Coincideix(text) || Isbn.Contains(text, StringComparison.OrdinalIgnoreCase);
}
}Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(llibre1.Coincideix("cortazar")); // True: coincideix en Autor
Console.WriteLine(llibre1.Coincideix("978-84-376-0495")); // True: coincideix en Isbn
Console.WriteLine(llibre1.Coincideix("harry potter")); // False
- Membres amb implementació per defecte (C# 8 en endavant)
Des de C# 8, una interfície pot incloure, opcionalment, una implementació per defecte per a
algun dels seus membres, fent servir el mateix cos { } (o =>) que en una classe. Qualsevol
classe que implementi la interfície hereta aquest comportament automàticament, sense haver-lo
d'escriure, tot i que encara pot sobreescriure'l si ho necessita:
interface IPrestable
{
bool Disponible { get; }
void Prestar();
void Retornar();
string EstatText() => Disponible ? "Disponible" : "Prestat"; // membre amb cos per defecte
}Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = llibre1;
Console.WriteLine(prestable.EstatText()); // "Disponible", sense que MaterialBibliotecari l'implementiAquesta característica es fa servir sobretot quan una biblioteca ja publicada necessita afegir un nou membre a una interfície existent sense trencar el codi de qui ja la implementava: gràcies al cos per defecte, les classes antigues continuen compilant sense canvis. En el codi d'aplicació quotidià (com el de BiblioTech) és una eina que apareix amb poca freqüència; n'hi ha prou de saber que existeix i reconèixer-la si la veus en codi d'altri.
- El model de BiblioTech amb interfícies aplicades
Amb els canvis d'aquest apartat, així queda MaterialBibliotecari implementant totes dues
interfícies, sense que res canviï en com es fan servir Llibre i Revista des de la resta del
programa:
classDiagram
class IPrestable {
<<interface>>
+bool Disponible
+Prestar()
+Retornar()
}
class ICercable {
<<interface>>
+Coincideix(string) bool
}
class MaterialBibliotecari {
<<abstract>>
+string Titol
+string Autor
+Descriure()* string
}
class Llibre {
+string Isbn
}
class Revista {
+int NumeroEdicio
}
IPrestable <|.. MaterialBibliotecari
ICercable <|.. MaterialBibliotecari
MaterialBibliotecari <|-- Llibre
MaterialBibliotecari <|-- Revista
La fletxa de puntets (<|..) és la notació habitual per a "implementa una interfície", diferent
de la fletxa contínua (<|--) d'herència entre classes que ja coneixes del Mòdul 3.
Errors Comuns i Consells
- Intentar donar cos a un membre d'interfície sense fer servir la sintaxi de membre per
defecte:
void Prestar();acaba en;, igual que un mètodeabstract; si escrius{ }directament a la interfície sense haver-ho pensat, en realitat estàs fent servir (a partir de C# 8) un membre per defecte, amb les implicacions de l'apartat 6. - Oblidar implementar algun membre de la interfície: si una classe declara
: IPrestableperò li falta, per exemple,Retornar(), el compilador dona un error assenyalant exactament quin membre falta per implementar. - Confondre el tipus de la variable amb el tipus real de l'objecte:
IPrestable prestable = llibre1;només permet accedir als membres d'IPrestable; per fer servirIsbnoDescriure()cal convertir de nou aLlibreambis/as. - Abusar d'interfícies amb un únic mètode: si una única classe implementa la interfície i no hi ha cap raó per desacoblar el contracte de la implementació, de vegades n'hi ha prou amb un mètode normal; les interfícies aporten més valor com més implementacions diferents puguin compartir el mateix contracte.
- Consell: anomena les interfícies sempre amb el prefix
I(IPrestable,ICercable,IComparable); és una convenció molt consolidada en C# i en tota la biblioteca de .NET, i facilita reconèixer d'una ullada que un tipus és una interfície.
Exercicis
-
Defineix la interfície
IPrestableambbool Disponible { get; },void Prestar();ivoid Retornar();. Fes queMaterialBibliotecarila implementi (recorda que els seus membres ja existents haurien de cobrir el contracte sense canvis). Crea unLlibre, assigna'l a una variableIPrestablei cridaPrestar()a través d'aquesta variable. -
Defineix la interfície
ICercableambbool Coincideix(string text);. ImplementaCoincideix()aMaterialBibliotecari(comparantTitoliAutor) i sobreescriu-lo aRevistaperquè també compari perNumeroEdicio.ToString(). Comprova el resultat amb diverses cerques d'exemple. -
Afegeix a
IPrestableun membre amb implementació per defectestring EstatText() => Disponible ? "Disponible" : "Prestat";. Crea unLlibre, presta'l, i mostra el resultat deEstatText()abans i després de prestar-lo, accedint sempre a través d'una variable de tipusIPrestable.
Solucions
interface IPrestable
{
bool Disponible { get; }
void Prestar();
void Retornar();
}
abstract class MaterialBibliotecari : IPrestable
{
// Titol, Autor, Disponible, constructor, Prestar(), Retornar() sense canvis
}
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = llibre1;
prestable.Prestar(); // "'Rayuela' ha estat prestat."
interface ICercable
{
bool Coincideix(string text);
}
abstract class MaterialBibliotecari : IPrestable, ICercable
{
// ...
public virtual bool Coincideix(string text)
{
return Titol.Contains(text, StringComparison.OrdinalIgnoreCase)
|| Autor.Contains(text, StringComparison.OrdinalIgnoreCase);
}
}
class Revista : MaterialBibliotecari
{
// ...
public override bool Coincideix(string text)
{
return base.Coincideix(text) || NumeroEdicio.ToString().Contains(text);
}
}
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
Console.WriteLine(revista1.Coincideix("302")); // True
Console.WriteLine(revista1.Coincideix("national")); // True
interface IPrestable
{
bool Disponible { get; }
void Prestar();
void Retornar();
string EstatText() => Disponible ? "Disponible" : "Prestat";
}
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = llibre1;
Console.WriteLine(prestable.EstatText()); // "Disponible"
prestable.Prestar();
Console.WriteLine(prestable.EstatText()); // "Prestat"
Conclusió
En aquesta lliçó has après a definir interfícies amb interface, a implementar-les en classes
existents (comprovant que MaterialBibliotecari ja complia la major part del contracte
d'IPrestable), a combinar diverses interfícies en una mateixa classe, i a reconèixer els
membres amb implementació per defecte de C# 8. MaterialBibliotecari queda ara com
abstract class MaterialBibliotecari : IPrestable, ICercable, amb Llibre i Revista
heretant i enriquint tots dos contractes.
Les interfícies resolen el "què pot fer un tipus", però hi ha una altra pregunta pendent a
BiblioTech: com reaccionar quan passa alguna cosa —per exemple, quan es registra un
préstec—, sense acoblar directament qui ho provoca amb qui se n'ha d'assabentar. A la propera
lliçó coneixeràs els delegats i els esdeveniments, l'eina de C# pensada exactament per a
això, i veuràs néixer la classe Biblioteca, que a les properes lliçons d'aquest mòdul passarà
a organitzar tot el catàleg i els socis.
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
