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

  1. Qué es una interfaz y qué problema resuelve
  2. Interfaces frente a clases abstractas: retomando la distinción del Módulo 3
  3. Definir e implementar una interfaz: IPrestable
  4. Implementación múltiple de interfaces
  5. Una segunda interfaz: IBuscable
  6. Miembros con implementación por defecto (C# 8 en adelante)
  7. El modelo de BiblioTech con interfaces aplicadas

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

interface IPrestable
{
    bool Disponible { get; }
    void Prestar();
    void Devolver();
}

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.

  1. 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 No
Campos con estado 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.

  1. Definir e implementar una interfaz: IPrestable

Implementar 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); // False

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

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

  1. Una segunda interfaz: IBuscable

Para 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):

interface IBuscable
{
    bool Coincide(string texto);
}

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

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

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

  1. 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étodo abstract; 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 : IPrestable pero 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 de IPrestable; para usar Isbn o Describir() hace falta convertir de nuevo a Libro con is/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

  1. Define la interfaz IPrestable con bool Disponible { get; }, void Prestar(); y void Devolver();. Haz que MaterialBibliotecario la implemente (recuerda que sus miembros ya existentes deberían cubrir el contrato sin cambios). Crea un Libro, asígnalo a una variable IPrestable y llama a Prestar() a través de esa variable.

  2. Define la interfaz IBuscable con bool Coincide(string texto);. Implementa Coincide() en MaterialBibliotecario (comparando Titulo y Autor) y sobrescríbelo en Revista para que también compare por NumeroEdicion.ToString(). Comprueba el resultado con varias búsquedas de ejemplo.

  3. Añade a IPrestable un miembro con implementación por defecto string EstadoTexto() => Disponible ? "Disponible" : "Prestado";. Crea un Libro, préstalo, y muestra el resultado de EstadoTexto() antes y después de prestarlo, accediendo siempre a través de una variable de tipo IPrestable.

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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados