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
- El problema: instanciar una clase que no debería existir por sí sola
- Clases abstractas: la palabra clave
abstract - Métodos abstractos frente a métodos virtuales
- Convirtiendo
MaterialBibliotecarioen clase abstracta MostrarFicha()sigue siendovirtual: la diferencia en la práctica- Cuándo usar una clase abstracta
- Abstracción frente a interfaces: una distinción conceptual
- 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.
- Clases abstractas: la palabra clave
abstract
abstractUna 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 abstractFí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.
- 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 | Sí | 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.
- Convirtiendo
MaterialBibliotecario en clase abstracta
MaterialBibliotecario en clase abstractaCon 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".
MostrarFicha() sigue siendo virtual: la diferencia en la práctica
MostrarFicha() sigue siendo virtual: la diferencia en la prácticaEs 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, aunqueLibroyRevistala enriquezcan añadiendo su dato particular. Por eso esvirtual: ofrece un comportamiento por defecto útil, que las herederas pueden ampliar si quieren.Describir(), en cambio, no tiene ninguna implementación razonable enMaterialBibliotecario: no hay una frase genérica que tenga sentido para "cualquier material bibliotecario" sin saber si es un libro o una revista. Por eso esabstract: 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.
- 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 aLibrooRevista). - 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 | Sí |
| Hay comportamiento común (con implementación) que varias herederas deben compartir | Sí |
| 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 |
- 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
abstractdirectamente: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
abstracten una clase heredera: siLibrono defineoverride string Describir(), el compilador da un error, a menos queLibrotambién se declare comoabstract(lo cual trasladaría la obligación a su propia heredera, en lugar de resolverla). - Poner cuerpo a un método
abstract: un métodoabstracttermina siempre en;, sin llaves{ }; si necesitas una implementación por defecto (aunque sea sencilla), lo correcto esvirtual, noabstract. - Marcar una clase como
abstractsin ningún métodoabstract: 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 seaabstract, 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
-
Declara
MaterialBibliotecariocomoabstract class, manteniendo sus propiedades (Titulo,Autor,Disponibleconprivate set) y su constructor. Comprueba quenew MaterialBibliotecario("...", "...")provoca un error de compilación si lo intentas. -
Añade a
MaterialBibliotecarioun métodopublic abstract string Describir();, y proporciona su implementación conoverridetanto enLibro(incluyendo el ISBN) como enRevista(incluyendo el número de edición). Crea un objeto de cada clase y muestra el resultado deDescribir(). -
Añade una tercera clase heredera,
Dvd : MaterialBibliotecario, con una propiedad propiaDuracionMinutos(int) y su propio constructor conbase(...). ImplementaDescribir()para que devuelva, por ejemplo,"Dvd: <Titulo> (<DuracionMinutos> min)". Añádelo a un arrayMaterialBibliotecario[]junto con unLibroy unaRevista, y recorre el array mostrando el resultado deDescribir()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#
- 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
