El Módulo 3 terminó con una promesa pendiente: en la lección de Abstracción viste que
abstract class y interface son "dos herramientas distintas para expresar abstracción", y
que las interfaces se estudiarían en detalle en este módulo. Ha llegado el momento. Una
interfaz es un contrato: una lista de miembros (métodos, propiedades, eventos) que una
clase se compromete a implementar, sin que la interfaz misma aporte ningún código. Esta
lección abre el Módulo 4 de Conceptos Avanzados definiendo dos interfaces para BiblioTech —
IPrestable e IBuscable— y aplicándolas sobre el modelo de dominio que ya conoces, como
primer paso hacia el resto de herramientas modernas de C# que verás en este módulo.
Contenido
- Qué es una interfaz y qué problema resuelve
- Interfaces frente a clases abstractas: retomando la distinción del Módulo 3
- Definir e implementar una interfaz:
IPrestable - Implementación múltiple de interfaces
- Una segunda interfaz:
IBuscable - Miembros con implementación por defecto (C# 8 en adelante)
- El modelo de BiblioTech con interfaces aplicadas
- Qué es una interfaz y qué problema resuelve
Una interfaz declara qué debe poder hacer un tipo, sin decir cómo lo hace. Se define
con la palabra clave interface, por convención con el prefijo I (IPrestable,
IBuscable, o las ya conocidas IEnumerable, IComparable de la propia biblioteca de
.NET), y enumera miembros sin ningún cuerpo de implementación:
Cualquier clase que declare : IPrestable se compromete, ante el compilador, a ofrecer una
propiedad Disponible (al menos con lectura), y los métodos Prestar() y Devolver() con
exactamente esas firmas. Si falta alguno, el código no compila. La interfaz en sí misma no
puede instanciarse (new IPrestable() no tiene sentido y no compila) ni contiene ningún
campo con estado propio: es pura forma, ningún contenido.
El problema que resuelve es el de desacoplar el contrato de la implementación: código que
solo necesita saber que algo "se puede prestar" puede trabajar con la interfaz IPrestable
sin importarle si por debajo hay un Libro, una Revista o cualquier tipo futuro que ni
siquiera exista todavía, siempre que cumpla el contrato.
- Interfaces frente a clases abstractas: retomando la distinción del Módulo 3
En la lección de Abstracción viste que una clase abstracta modela una relación "es un" con herencia de código compartido, mientras que una interfaz modela un contrato de "puede hacer" sin ninguna implementación compartida. Con las interfaces ya definidas formalmente, esta tabla recoge la comparación completa:
Clase abstracta (abstract class) |
Interfaz (interface) |
|
|---|---|---|
| Relación que modela | "Es un" (Libro es un MaterialBibliotecario) |
"Puede hacer" (Libro puede prestarse) |
| Implementación compartida | Sí, en los miembros no abstract |
No, salvo miembros por defecto (apartado 6) |
| Constructor propio | Sí | No |
| Campos con estado | Sí | No |
| Cuántas puede combinar una clase | Una única clase base | Varias interfaces a la vez |
| Ejemplo en BiblioTech | MaterialBibliotecario |
IPrestable, IBuscable |
La regla práctica para decidir entre ambas: si necesitas compartir código real (propiedades ya
implementadas, métodos con cuerpo) entre clases relacionadas por herencia, usa una clase
abstracta; si solo necesitas garantizar que varios tipos —relacionados entre sí o no— ofrezcan
ciertas capacidades, sin compartir ninguna implementación, usa una interfaz. De hecho, ambas
herramientas no son excluyentes: en el siguiente apartado verás que MaterialBibliotecario
sigue siendo abstract class y, además, implementa interfaces.
- Definir e implementar una interfaz:
IPrestable
IPrestableImplementar una interfaz consiste en declararla tras el nombre de la clase (después de la
clase base, si la hay, separadas por comas) y asegurarse de que todos sus miembros están
cubiertos por miembros public con la misma firma:
abstract class MaterialBibliotecario : IPrestable
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; private set; } = true;
public MaterialBibliotecario(string titulo, string autor)
{
Titulo = titulo;
Autor = autor;
}
public void Prestar()
{
if (Disponible)
{
Disponible = false;
Console.WriteLine($"'{Titulo}' ha sido prestado.");
}
else
{
Console.WriteLine($"'{Titulo}' no esta disponible para prestamo.");
}
}
public void Devolver()
{
Disponible = true;
Console.WriteLine($"'{Titulo}' ha sido devuelto.");
}
public virtual void MostrarFicha()
{
Console.WriteLine($"Titulo: {Titulo}");
Console.WriteLine($"Autor: {Autor}");
Console.WriteLine($"Disponible: {Disponible}");
}
public abstract string Describir();
}Fíjate en un detalle importante: MaterialBibliotecario ya tenía Disponible, Prestar() y
Devolver() desde el Módulo 3, con exactamente el aspecto que IPrestable exige. No hizo
falta escribir ni una línea de código nueva para "cumplir" la interfaz: bastó con añadir
: IPrestable y el compilador comprueba que los miembros ya existentes encajan con el
contrato. Esto es habitual: las interfaces suelen formalizar capacidades que una clase ya
tenía, dándoles un nombre explícito y reutilizable.
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = libro1; // valido: Libro es-un MaterialBibliotecario, que es-un IPrestable
prestable.Prestar();
Console.WriteLine(prestable.Disponible); // FalseUna variable de tipo interfaz (IPrestable prestable) solo puede usar los miembros
declarados en la interfaz (Disponible, Prestar(), Devolver()), aunque el objeto
subyacente sea un Libro con muchos más miembros propios (Isbn, Describir()...); para
acceder a esos otros miembros haría falta convertir de nuevo a Libro, con is/as, como ya
viste en la lección de Polimorfismo.
- Implementación múltiple de interfaces
A diferencia de la herencia de clases (una clase solo puede tener una clase base directa), una clase puede implementar varias interfaces a la vez, separadas por comas:
class Ejemplo : ClaseBase, IPrestable, IBuscable, IComparable<Ejemplo>
{
// debe cumplir el contrato de las tres interfaces, ademas de heredar de ClaseBase
}Esta es una de las ventajas prácticas más citadas de las interfaces frente a las clases
abstractas: permiten combinar capacidades independientes entre sí (algo puede ser, a la vez,
"prestable", "buscable" y "comparable") sin forzar una única jerarquía de herencia rígida. En
el siguiente apartado, MaterialBibliotecario pasará a implementar dos interfaces distintas
al mismo tiempo.
- Una segunda interfaz:
IBuscable
IBuscablePara completar el ejemplo, se define una segunda interfaz pensada para las búsquedas en el catálogo de BiblioTech (una capacidad que usarás de forma más completa en la lección de LINQ, más adelante en este módulo):
MaterialBibliotecario implementa ambas interfaces a la vez, y declara Coincide() como
virtual (no forma parte del contrato de la interfaz que un método deba ser virtual, pero
nada lo impide: es una decisión de diseño para que Libro y Revista puedan enriquecer la
búsqueda con sus propios datos):
abstract class MaterialBibliotecario : IPrestable, IBuscable
{
// ... Titulo, Autor, Disponible, constructor, Prestar(), Devolver(), MostrarFicha() ...
public abstract string Describir();
public virtual bool Coincide(string texto)
{
return Titulo.Contains(texto, StringComparison.OrdinalIgnoreCase)
|| Autor.Contains(texto, StringComparison.OrdinalIgnoreCase);
}
}Libro sobrescribe Coincide() para que la búsqueda también tenga en cuenta el ISBN,
reutilizando la implementación de la clase base con base.Coincide(texto):
class Libro : MaterialBibliotecario
{
public string Isbn { get; set; }
public Libro(string titulo, string autor, string isbn) : base(titulo, autor)
{
Isbn = isbn;
}
public override string Describir()
{
return $"Libro: {Titulo}, de {Autor} (ISBN {Isbn})";
}
public override bool Coincide(string texto)
{
return base.Coincide(texto) || Isbn.Contains(texto, StringComparison.OrdinalIgnoreCase);
}
}Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(libro1.Coincide("cortazar")); // True: coincide en Autor
Console.WriteLine(libro1.Coincide("978-84-376-0495")); // True: coincide en Isbn
Console.WriteLine(libro1.Coincide("harry potter")); // False
- Miembros con implementación por defecto (C# 8 en adelante)
Desde C# 8, una interfaz puede incluir, opcionalmente, una implementación por defecto para
alguno de sus miembros, usando el mismo cuerpo { } (o =>) que en una clase. Cualquier
clase que implemente la interfaz hereda ese comportamiento automáticamente, sin tener que
escribirlo, aunque sigue pudiendo sobrescribirlo si lo necesita:
interface IPrestable
{
bool Disponible { get; }
void Prestar();
void Devolver();
string EstadoTexto() => Disponible ? "Disponible" : "Prestado"; // miembro con cuerpo por defecto
}Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = libro1;
Console.WriteLine(prestable.EstadoTexto()); // "Disponible", sin que MaterialBibliotecario lo implementeEsta característica se usa sobre todo cuando una biblioteca ya publicada necesita añadir un nuevo miembro a una interfaz existente sin romper el código de quienes ya la implementaban: gracias al cuerpo por defecto, las clases antiguas siguen compilando sin cambios. En el código de aplicación cotidiano (como el de BiblioTech) es una herramienta que aparece con poca frecuencia; basta con saber que existe y reconocerla si la ves en código ajeno.
- El modelo de BiblioTech con interfaces aplicadas
Con los cambios de este apartado, así queda MaterialBibliotecario implementando ambas
interfaces, sin que nada cambie en cómo se usan Libro y Revista desde el resto del
programa:
classDiagram
class IPrestable {
<<interface>>
+bool Disponible
+Prestar()
+Devolver()
}
class IBuscable {
<<interface>>
+Coincide(string) bool
}
class MaterialBibliotecario {
<<abstract>>
+string Titulo
+string Autor
+Describir()* string
}
class Libro {
+string Isbn
}
class Revista {
+int NumeroEdicion
}
IPrestable <|.. MaterialBibliotecario
IBuscable <|.. MaterialBibliotecario
MaterialBibliotecario <|-- Libro
MaterialBibliotecario <|-- Revista
La flecha punteada (<|..) es la notación habitual para "implementa una interfaz", distinta
de la flecha continua (<|--) de herencia entre clases que ya conoces del Módulo 3.
Errores Comunes y Consejos
- Intentar dar cuerpo a un miembro de interfaz sin usar la sintaxis de miembro por
defecto:
void Prestar();termina en;, igual que un métodoabstract; si escribes{ }directamente en la interfaz sin haberlo pensado, en realidad estás usando (a partir de C# 8) un miembro por defecto, con las implicaciones del apartado 6. - Olvidar implementar algún miembro de la interfaz: si una clase declara
: IPrestablepero le falta, por ejemplo,Devolver(), el compilador da un error señalando exactamente qué miembro falta por implementar. - Confundir el tipo de la variable con el tipo real del objeto:
IPrestable prestable = libro1;solo permite acceder a los miembros deIPrestable; para usarIsbnoDescribir()hace falta convertir de nuevo aLibroconis/as. - Abusar de interfaces con un único método: si una única clase implementa la interfaz y no hay ninguna razón para desacoplar el contrato de la implementación, a veces basta con un método normal; las interfaces aportan más valor cuantas más implementaciones distintas puedan compartir el mismo contrato.
- Consejo: nombra las interfaces siempre con el prefijo
I(IPrestable,IBuscable,IComparable); es una convención muy consolidada en C# y en toda la biblioteca de .NET, y facilita reconocer de un vistazo que un tipo es una interfaz.
Ejercicios
-
Define la interfaz
IPrestableconbool Disponible { get; },void Prestar();yvoid Devolver();. Haz queMaterialBibliotecariola implemente (recuerda que sus miembros ya existentes deberían cubrir el contrato sin cambios). Crea unLibro, asígnalo a una variableIPrestabley llama aPrestar()a través de esa variable. -
Define la interfaz
IBuscableconbool Coincide(string texto);. ImplementaCoincide()enMaterialBibliotecario(comparandoTituloyAutor) y sobrescríbelo enRevistapara que también compare porNumeroEdicion.ToString(). Comprueba el resultado con varias búsquedas de ejemplo. -
Añade a
IPrestableun miembro con implementación por defectostring EstadoTexto() => Disponible ? "Disponible" : "Prestado";. Crea unLibro, préstalo, y muestra el resultado deEstadoTexto()antes y después de prestarlo, accediendo siempre a través de una variable de tipoIPrestable.
Soluciones
interface IPrestable
{
bool Disponible { get; }
void Prestar();
void Devolver();
}
abstract class MaterialBibliotecario : IPrestable
{
// Titulo, Autor, Disponible, constructor, Prestar(), Devolver() sin cambios
}
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = libro1;
prestable.Prestar(); // "'Rayuela' ha sido prestado."
interface IBuscable
{
bool Coincide(string texto);
}
abstract class MaterialBibliotecario : IPrestable, IBuscable
{
// ...
public virtual bool Coincide(string texto)
{
return Titulo.Contains(texto, StringComparison.OrdinalIgnoreCase)
|| Autor.Contains(texto, StringComparison.OrdinalIgnoreCase);
}
}
class Revista : MaterialBibliotecario
{
// ...
public override bool Coincide(string texto)
{
return base.Coincide(texto) || NumeroEdicion.ToString().Contains(texto);
}
}
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);
Console.WriteLine(revista1.Coincide("302")); // True
Console.WriteLine(revista1.Coincide("national")); // True
interface IPrestable
{
bool Disponible { get; }
void Prestar();
void Devolver();
string EstadoTexto() => Disponible ? "Disponible" : "Prestado";
}
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
IPrestable prestable = libro1;
Console.WriteLine(prestable.EstadoTexto()); // "Disponible"
prestable.Prestar();
Console.WriteLine(prestable.EstadoTexto()); // "Prestado"
Conclusión
En esta lección has aprendido a definir interfaces con interface, a implementarlas en clases
existentes (comprobando que MaterialBibliotecario ya cumplía la mayor parte del contrato de
IPrestable), a combinar varias interfaces en una misma clase, y a reconocer los miembros con
implementación por defecto de C# 8. MaterialBibliotecario queda ahora como
abstract class MaterialBibliotecario : IPrestable, IBuscable, con Libro y Revista
heredando y enriqueciendo ambos contratos.
Las interfaces resuelven el "qué puede hacer un tipo", pero hay otra pregunta pendiente en
BiblioTech: cómo reaccionar cuando algo ocurre —por ejemplo, cuando se registra un
préstamo—, sin acoplar directamente quién lo provoca con quién debe enterarse. En la próxima
lección conocerás los delegados y los eventos, la herramienta de C# pensada
exactamente para eso, y verás nacer la clase Biblioteca, que en las próximas lecciones de
este módulo pasará a organizar todo el catálogo y los socios.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
