1. Métodos abstractos
En la lección anterior pusimos la base: conocimos las clases abstractas y vimos que pueden contener métodos y propiedades abstractas. Entendimos que esto es un "contrato" que obliga a las clases hijas a dar su propia implementación.
Ahora vamos a profundizar en los detalles y ver escenarios más complejos y prácticos. En esta lección nos vamos a centrar en los matices de la sintaxis, en cómo funciona la abstracción en jerarquías multinivel y vamos a dejar claro cuándo usar abstract y cuándo — virtual.
Un método abstracto es como una receta misteriosa de un libro de cocina antiguo: "añade un ingrediente secreto" — cuál, no se dice, ¡pero todos los cocineros siguientes tienen que inventar algo!
Un método abstracto siempre está en una clase abstracta
Intentar declarar un método abstracto en una clase normal (no abstracta) da error de compilación. ¿Por qué? Porque una clase normal se puede crear directamente, ¿y qué pasa si intentamos llamar a un método que no está definido? Solo una mirada misteriosa del IDE.
// Esto dará error de compilación:
public class WrongClass
{
public abstract void Oops(); // ¡No se puede hacer esto!
}
Si una clase tiene al menos un método abstracto, ¡tiene que estar marcada como abstract!
Implementación de métodos abstractos en clases derivadas
La implementación se hace con la palabra clave override. Una clase que hereda de una clase abstracta y no implementa todos los métodos abstractos también tiene que ser abstracta (si no, el compilador se enfada).
Sigamos con la idea de nuestra app:
En las lecciones anteriores creamos la clase Shape (figura), donde declaramos un método abstracto para calcular el área. Ahora vamos a crear figuras concretas:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
¿Por qué son tan importantes los métodos abstractos?
Los métodos abstractos son tu forma de decir: "Chicos, si heredáis esta clase, ¡decidid cómo tiene que funcionar esto!" Esto hace que la arquitectura del programa sea más clara, te protege de olvidos accidentales y, lo más importante, te permite usar el polimorfismo a tope.
2. Propiedades abstractas (properties)
¿Qué es una propiedad abstracta?
Una propiedad abstracta es un contrato igual que un método abstracto, pero para una propiedad (property). Para C# esto es muy natural, porque las propiedades son una de las formas principales de encapsular datos. Una propiedad abstracta dice: "Deja que los hijos decidan por sí mismos de dónde sacar (y, si hace falta, cómo poner) el valor de esta propiedad".
Ejemplo:
Añadimos la propiedad Name para la figura — todos los hijos deben tenerla, pero que cada uno decida qué devolver.
public abstract class Shape
{
public abstract string Name { get; }
public abstract double CalculateArea();
}
Ahora cada hijo tiene que implementar esta propiedad:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public override string Name => "Rectángulo";
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public override string Name => "Círculo";
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
Particularidades de la sintaxis
Una propiedad abstracta se parece a una de interfaz: se declara sin cuerpo de implementación, pero indicando si tendrá solo getter o también setter.
public abstract class Creature
{
// propiedad solo lectura
public abstract string Species { get; }
// propiedad lectura-escritura
public abstract int Age { get; set; }
}
En la clase que implementa puedes definir la lógica para obtener (y si hace falta — cambiar) el valor.
¿Para qué sirven las propiedades abstractas?
Una propiedad abstracta es una herramienta genial cuando quieres que cada hijo decida cómo obtener o guardar el valor. Esto se usa mucho en modelado, en modelos de negocio, view-models, y en general donde la lógica de la propiedad es más compleja que solo devolver un campo.
Por ejemplo, si una figura calcula el nombre con una fórmula y otra lo devuelve como constante, todo eso se puede "esconder" bonito detrás de una propiedad abstracta.
3. Esquema: cómo se relacionan clase abstracta, métodos y propiedades
Vamos a ver el siguiente esquema para ver cómo funciona:
┌─────────────┐
│ abstract │
│ Shape │
│-------------│
│ +Name: str │ <-- propiedad abstracta
│ +Area(): dbl│ <-- método abstracto
└─────┬───────┘
│
┌────▼────┐ ┌───────┐
│Rectangle│ │ Circle│
│........ │ .... │ ......│
│+Name │ │+Name │
│+Area() │ │+Area()│
└─────────┘ └───────┘
Así aseguramos UNA interfaz para trabajar con cualquier hijo de la clase Shape, pero cada hijo puede hacer "bajo el capó" lo que quiera.
Diagrama UML para método y propiedad abstracta:
┌────────────────────────────┐
│ abstract class │
│ Animal │
│────────────────────────────│
│+ Name: string {abstract} │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
│
┌────────┴──────────┐
│ │
┌────────────┐ ┌─────────────┐
│ Cat │ │ Dog │
│────────────│ │─────────────│
│+ Name │ │+ Name │
│+ MakeSound()│ │+ MakeSound()│
└────────────┘ └─────────────┘
4. Escenarios prácticos y utilidad en proyectos reales
Los métodos y propiedades abstractas son tu herramienta si diseñas jerarquías de clases que deben evolucionar. Es la base para grandes aplicaciones de negocio, modelos de dominio multinivel, sistemas de plugins, frameworks de UI, donde para las bases del sistema se define una "ley" obligatoria para todos los hijos.
En proyectos reales, estas soluciones arquitectónicas "limpias" permiten que, después de meses y años, puedas meter nuevas entidades y funciones sabiendo que el código antiguo ya contempla todos los casos de comportamiento.
La pregunta "¿para qué?" y vuelta al polimorfismo
El polimorfismo parece magia, pero en realidad es solo un contrato: "puedes llamar a cierto método en cualquier objeto de la familia y siempre tendrás el resultado adecuado". Los métodos y propiedades abstractas son la forma de obligar a toda la jerarquía a hablar el mismo idioma, pero sin una implementación rígida "por defecto".
Este enfoque se usa mucho en sistemas de plugins (IDE, editores gráficos, sistemas CRM), cuando desarrolladores externos crean sus extensiones pero tienen que implementar un mínimo de funciones que exige la plataforma. Por ejemplo, si escribes un módulo para procesar archivos, la clase base puede tener una propiedad abstracta FileExtension y un método abstracto Open(), para que cualquier plugin pueda manejar bien los archivos de su tipo.
Diferencias entre miembros abstractos, virtuales y normales de clase
| Característica | Método/propiedad normal | virtual | abstract |
|---|---|---|---|
| ¿Tiene implementación? | Sí | Sí | No |
| ¿Override obligatorio? | No | No (se puede sobreescribir) | Sí (obligatorio en el hijo) |
| ¿Se puede llamar directamente? | Sí | Sí | No |
| ¿Puede estar en clase normal? | Sí | Sí | No |
| ¿Puede ser marcado sealed? | No | Sí | No |
5. Aplicación en nuestra app de aprendizaje
Ahora, con los nuevos conocimientos, volvamos a nuestra app de figuras y veamos cómo las propiedades y métodos abstractos trabajan juntos para crear un sistema flexible y claro.
// Por ejemplo, en el método principal del programa
List<Shape> shapes = new List<Shape>
{
new Rectangle(4, 5),
new Circle(2.5)
};
foreach (Shape shape in shapes)
{
// Gracias a la propiedad abstracta Name y al método CalculateArea,
// no nos importa el tipo concreto de figura
Console.WriteLine($"{shape.Name}, Área: {shape.CalculateArea():F2}");
}
Resultado:
Rectángulo, Área: 20.00
Círculo, Área: 19.63
Qué bonito: el código no sabe ni le importa qué figura es — solo llama a las propiedades y métodos necesarios, ¡y el CLR de .NET se encarga de la implementación correcta!
6. Errores frecuentes y particularidades de implementación
Error nº1: método abstracto del padre no implementado.
Si en la clase hija queda algún método o propiedad abstracta sin implementar, y la clase no está marcada como abstract, el compilador no deja compilar el proyecto. Es una protección útil: no puedes crear un objeto con lógica "agujereada" — por ejemplo, una figura sin método CalculateArea().
Error nº2: método declarado como abstract, pero la clase no.
Ese código tampoco compila. Si añades un método abstract a una clase, la clase misma debe ser abstracta:
public abstract class Polygon : Shape
{
// No implementamos CalculateArea(), la clase sigue siendo abstract
}
Error nº3: intentar implementar un método abstracto con virtual.
Un método abstracto debe implementarse con la palabra clave override, no con virtual. Pero si quieres que la implementación se pueda sobreescribir más abajo en la jerarquía, puedes primero sobreescribir el método y luego declararlo como virtual en la clase hija:
public override double CalculateArea()
{
// Implementación por defecto...
}
En un hijo más profundo ese método ya se puede sobreescribir otra vez. Pero volver a hacerlo abstract — no se puede, porque ya hay implementación.
GO TO FULL VERSION