La lección anterior terminó señalando un detalle incómodo: nada impide escribir new MaterialBibliotecario("Titulo cualquiera", "Autor cualquiera") en cualquier parte del programa, a pesar de que "un material bibliotecario" sin más, que no sea ni un libro ni una revista, no representa ningún objeto real del catálogo de BiblioTech. MaterialBibliotecario siempre se pensó como una base común, nunca como algo que debiera existir por sí solo. La abstracción es el principio que te permite expresar esto directamente en el código: declarar que una clase es puramente conceptual, una plantilla para sus herederas, y que nunca debe instanciarse por sí misma. En esta lección convertirás MaterialBibliotecario en una clase abstract, y su método Describir() en un método abstract que cada heredera está obligada a implementar.

Contenido

  1. El problema: instanciar una clase que no debería existir por sí sola
  2. Clases abstractas: la palabra clave abstract
  3. Métodos abstractos frente a métodos virtuales
  4. Convirtiendo MaterialBibliotecario en clase abstracta
  5. MostrarFicha() sigue siendo virtual: la diferencia en la práctica
  6. Cuándo usar una clase abstracta
  7. Abstracción frente a interfaces: una distinción conceptual

  1. El problema: instanciar una clase que no debería existir por sí sola

Con el diseño actual, este código compila y se ejecuta sin ningún error:

MaterialBibliotecario materialGenerico = new MaterialBibliotecario("Sin titulo", "Sin autor");
Console.WriteLine(materialGenerico.Describir()); // "Material: Sin titulo, de Sin autor"

Nada en el código impide esto, aunque conceptualmente no tenga sentido: en el dominio real de BiblioTech, todo lo que se presta es siempre, específicamente, un libro o una revista (o, en el futuro, otro tipo concreto), nunca "un material" sin más especificar. MaterialBibliotecario existe únicamente para que Libro y Revista compartan código; permitir que se instancie directamente es una posibilidad que el diseño debería cerrar explícitamente, no dejar abierta por descuido.

  1. Clases abstractas: la palabra clave abstract

Una clase declarada con abstract no puede instanciarse directamente con new; solo puede usarse como clase base de otras clases (no abstractas) que hereden de ella:

abstract class MaterialBibliotecario
{
    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.");
    }
}
// MaterialBibliotecario materialGenerico = new MaterialBibliotecario("...", "...");
// Error de compilacion: no se puede crear una instancia de la clase abstracta 'MaterialBibliotecario'

Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"); // Ok: Libro no es abstract

Fíjate en un detalle importante: aunque MaterialBibliotecario ya no se pueda instanciar directamente, sigue teniendo un constructor, y ese constructor sigue ejecutándose —a través de base(titulo, autor)— cada vez que se crea un Libro o una Revista. Una clase abstracta puede tener constructores, campos, propiedades y métodos con implementación completa, exactamente igual que una clase normal; lo único que cambia es que ella misma no puede convertirse en un objeto independiente.

  1. Métodos abstractos frente a métodos virtuales

Además de marcar la clase entera como abstract, se puede marcar un método como abstract. Un método abstracto declara su firma (nombre, parámetros, tipo de retorno) pero no tiene cuerpo: no hay ninguna implementación en la clase base, y cada clase heredera está obligada a proporcionar la suya con override.

abstract class MaterialBibliotecario
{
    // ... propiedades, constructor, Prestar(), Devolver() como antes ...

    public abstract string Describir(); // sin cuerpo, termina con ";"
}

Esto es distinto de virtual, que sí exige (u ofrece) una implementación por defecto en la clase base, y deja la sobreescritura como opcional para las herederas.

virtual abstract
Implementación en la clase base Obligatoria (tiene cuerpo) Prohibida (solo la firma, termina en ;)
Sobreescribir en la clase heredera Opcional Obligatorio
Se puede usar en una clase no abstracta No; una clase con algún miembro abstract debe ser, ella misma, abstract
Ejemplo en BiblioTech MostrarFicha() Describir()

Si Libro o Revista no proporcionaran su propia implementación de Describir(), el compilador daría un error: al ser abstract, no existe ninguna versión "por defecto" a la que recurrir.

  1. Convirtiendo MaterialBibliotecario en clase abstracta

Con estos dos cambios (la clase abstract y Describir() como método abstract), así queda el modelo completo de la jerarquía de materiales de BiblioTech:

abstract class MaterialBibliotecario
{
    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();
}

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})";
    }
}

class Revista : MaterialBibliotecario
{
    public int NumeroEdicion { get; set; }

    public Revista(string titulo, string autor, int numeroEdicion) : base(titulo, autor)
    {
        NumeroEdicion = numeroEdicion;
    }

    public override string Describir()
    {
        return $"Revista: {Titulo}, edicion numero {NumeroEdicion}";
    }
}

El resto del código que ya escribiste en la lección de Polimorfismo —el array MaterialBibliotecario[] recorrido con foreach, is, as— sigue funcionando exactamente igual: la abstracción no cambia cómo se usan los objetos Libro y Revista, solo impide que se cree un MaterialBibliotecario "a secas".

  1. MostrarFicha() sigue siendo virtual: la diferencia en la práctica

Es intencionado que Describir() sea abstract pero MostrarFicha() siga siendo solo virtual: son dos situaciones distintas que ilustran cuándo usar cada una.

  • MostrarFicha() tiene sentido con una implementación genérica razonable (mostrar título, autor y disponibilidad) que sirve tal cual para cualquier material, aunque Libro y Revista la enriquezcan añadiendo su dato particular. Por eso es virtual: ofrece un comportamiento por defecto útil, que las herederas pueden ampliar si quieren.
  • Describir(), en cambio, no tiene ninguna implementación razonable en MaterialBibliotecario: no hay una frase genérica que tenga sentido para "cualquier material bibliotecario" sin saber si es un libro o una revista. Por eso es abstract: obliga a que cada tipo concreto decida cómo describirse, sin ofrecer (ni permitir) una versión por defecto que no encajaría bien en ningún caso real.

Esta distinción —¿existe una implementación por defecto razonable, o no la hay?— es la pregunta clave para decidir si un miembro debe ser virtual o abstract.

  1. Cuándo usar una clase abstracta

Conviene declarar una clase como abstract cuando se cumplen, a la vez, estas condiciones:

  • La clase representa un concepto general que solo tiene sentido a través de sus especializaciones concretas (como MaterialBibliotecario, frente a Libro o Revista).
  • Quieres compartir código común (propiedades, métodos con implementación) entre varias clases relacionadas, evitando duplicación —el mismo motivo que ya viste en la lección de Herencia.
  • Quieres forzar, a nivel de compilador, que ciertos métodos se implementen en cada heredera concreta, sin arriesgarte a que alguien olvide hacerlo.
Señal ¿Clase abstracta?
La clase nunca debería instanciarse por sí sola, solo sus herederas
Hay comportamiento común (con implementación) que varias herederas deben compartir
Algunos métodos no tienen una implementación razonable sin saber el tipo concreto Sí, esos métodos como abstract
Todas las instancias posibles de este concepto son, en la práctica, del mismo tipo exacto (no hay variantes) No; una clase normal basta

  1. Abstracción frente a interfaces: una distinción conceptual

C# ofrece otra herramienta, muy relacionada, para expresar abstracción: las interfaces (interface). La diferencia conceptual esencial es esta: una clase abstracta modela una relación de tipo "es un" con herencia de código compartido (Libro es un MaterialBibliotecario, y hereda su Prestar()/Devolver() ya implementados), mientras que una interfaz modela un contrato de "puede hacer" sin ninguna implementación compartida ni relación de herencia de una única clase base. Además, una clase en C# solo puede heredar de una clase abstracta (recuerda la herencia simple del Módulo 3), pero puede implementar varias interfaces distintas a la vez.

En este módulo no profundizaremos más en las interfaces: se estudian en detalle, con toda su sintaxis y sus casos de uso, en el Módulo 4: Conceptos Avanzados de C#, justo en la primera lección. Por ahora, quédate con la idea de que abstract class y interface son dos herramientas distintas para expresar abstracción, y que BiblioTech usará ambas más adelante, cada una donde tenga más sentido.

Errores Comunes y Consejos

  • Intentar instanciar una clase abstract directamente: new MaterialBibliotecario(...) no compila nunca, sea cual sea el constructor que tenga definido; solo se puede crear instancias de sus clases herederas no abstractas (Libro, Revista).
  • Olvidar implementar un método abstract en una clase heredera: si Libro no define override string Describir(), el compilador da un error, a menos que Libro también se declare como abstract (lo cual trasladaría la obligación a su propia heredera, en lugar de resolverla).
  • Poner cuerpo a un método abstract: un método abstract termina siempre en ;, sin llaves { }; si necesitas una implementación por defecto (aunque sea sencilla), lo correcto es virtual, no abstract.
  • Marcar una clase como abstract sin ningún método abstract: es válido (simplemente impide instanciarla directamente), pero si no hay ningún miembro que fuerce a implementar algo distinto en cada heredera, conviene preguntarse si de verdad hace falta que sea abstract, o si bastaría con una clase normal que simplemente no se instancia en la práctica.
  • Confundir clase abstracta con interfaz: si necesitas compartir implementación real entre clases relacionadas por herencia, usa una clase abstracta; si solo necesitas garantizar que varios tipos no relacionados entre sí ofrezcan ciertos métodos, sin compartir código, una interfaz (Módulo 4) suele encajar mejor.

Ejercicios

  1. Declara MaterialBibliotecario como abstract class, manteniendo sus propiedades (Titulo, Autor, Disponible con private set) y su constructor. Comprueba que new MaterialBibliotecario("...", "...") provoca un error de compilación si lo intentas.

  2. Añade a MaterialBibliotecario un método public abstract string Describir();, y proporciona su implementación con override tanto en Libro (incluyendo el ISBN) como en Revista (incluyendo el número de edición). Crea un objeto de cada clase y muestra el resultado de Describir().

  3. Añade una tercera clase heredera, Dvd : MaterialBibliotecario, con una propiedad propia DuracionMinutos (int) y su propio constructor con base(...). Implementa Describir() para que devuelva, por ejemplo, "Dvd: <Titulo> (<DuracionMinutos> min)". Añádelo a un array MaterialBibliotecario[] junto con un Libro y una Revista, y recorre el array mostrando el resultado de Describir() de cada uno.

Soluciones

abstract class MaterialBibliotecario
{
    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;
    }
}

// MaterialBibliotecario m = new MaterialBibliotecario("X", "Y");
// Error de compilacion: no se puede crear una instancia de la clase abstracta
public abstract string Describir();

// En Libro:
public override string Describir()
{
    return $"Libro: {Titulo}, de {Autor} (ISBN {Isbn})";
}

// En Revista:
public override string Describir()
{
    return $"Revista: {Titulo}, edicion numero {NumeroEdicion}";
}

Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Revista revista1 = new Revista("National Geographic", "Varios autores", 302);

Console.WriteLine(libro1.Describir());
Console.WriteLine(revista1.Describir());
class Dvd : MaterialBibliotecario
{
    public int DuracionMinutos { get; set; }

    public Dvd(string titulo, string autor, int duracionMinutos) : base(titulo, autor)
    {
        DuracionMinutos = duracionMinutos;
    }

    public override string Describir()
    {
        return $"Dvd: {Titulo} ({DuracionMinutos} min)";
    }
}

MaterialBibliotecario[] catalogo = new MaterialBibliotecario[]
{
    new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
    new Revista("National Geographic", "Varios autores", 302),
    new Dvd("El Padrino", "Francis Ford Coppola", 175)
};

foreach (MaterialBibliotecario material in catalogo)
{
    Console.WriteLine(material.Describir());
}

Este ejercicio comprueba, en la práctica, la ventaja del polimorfismo combinado con abstracción: el bucle foreach no cambia en absoluto al añadir Dvd como tercer tipo de material, y cada uno se describe correctamente según su propia implementación.

Conclusión

En esta lección has terminado de cerrar el diseño de la jerarquía de materiales de BiblioTech: MaterialBibliotecario es ahora una clase abstract que no puede instanciarse directamente, con Describir() como método abstract que obliga a cada heredera concreta (Libro, Revista) a definir su propia descripción, mientras que MostrarFicha() se mantiene virtual por tener una implementación por defecto razonable. También has visto la diferencia conceptual entre una clase abstracta y una interfaz, que estudiarás en detalle en el Módulo 4.

Con esto se completa el diseño orientado a objetos del modelo de dominio de BiblioTech: MaterialBibliotecario (abstracta) con Libro y Revista como herederas concretas, Socio y Prestamo completando el resto. Queda una última pieza por resolver en este módulo: hasta ahora, todo lo que has modelado son clases, es decir, tipos por referencia. En la próxima lección, la última del módulo, conocerás los structs y los records, alternativas más ligeras para datos pequeños e inmutables (como una fecha de préstamo o la dirección de un socio), y entenderás cuándo conviene elegir cada una frente a una clase tradicional.

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