Interfaces y Clases Abstractas en C#
Aprende interfaces y clases abstractas en C#, sus diferencias y cuándo usar cada una para diseñar código limpio y extensible.
En C#, las clases abstractas y las interfaces se utilizan para construir arquitecturas orientadas a objetos flexibles y extensibles.
Ambas ayudan a definir comportamientos comunes, pero existen diferencias importantes.
Una abstract class puede proporcionar una implementación parcial (cuerpos de métodos), mientras que una interface solo declara
las firmas y deja la implementación completamente a las clases derivadas.
Clase Abstracta
Una clase abstracta se define con la palabra clave abstract y no puede ser instanciada directamente.
Define propiedades y métodos comunes, pero algunos métodos pueden marcarse como abstract para obligar a las subclases a implementarlos.
Las clases abstractas también pueden incluir métodos normales — es decir, comportamiento parcialmente implementado.
public abstract class Shape
{
public string Color { get; set; } = "Negro";
// Debe ser implementado por las subclases
public abstract double GetArea();
// Un método común
public void PrintColor()
{
Console.WriteLine($"Color: {Color}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
Aquí, la clase abstracta Shape proporciona una propiedad común Color,
pero deja la implementación de GetArea() a las subclases.
La clase Circle la implementa a su manera.
Interface
Una interfaz se define con la palabra clave interface.
Contiene solo firmas de métodos y propiedades, pero no cuerpos (antes de C# 8).
Una clase puede implementar varias interfaces al mismo tiempo (permitiendo una forma de herencia múltiple).
Esto establece un “contrato común” entre diferentes clases.
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Registrado en archivo: {message}");
}
}
public class DbLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Registrado en base de datos: {message}");
}
}
Las clases que implementan ILogger realizan el registro de diferentes maneras,
pero todas deben exponer la misma firma de método: Log(string).
Clase Abstracta vs Interface
Diferencias clave:
- Clase Abstracta: Puede definir comportamiento común y obligatorio. Puede contener propiedades y cuerpos de métodos.
- Interface: Solo declara firmas. Soporta múltiples implementaciones.
- Clase Abstracta: Una clase solo puede heredar de una clase abstracta (base).
- Interface: Una clase puede implementar múltiples interfaces.
- Clase Abstracta: Puede contener estado compartido, campos y un constructor.
- Interface: No contiene campos ni constructor.
Escenario Real: Línea de Producción (Interface + Clase Abstracta)
En el siguiente ejemplo, definimos una interfaz IProduct para modelar un flujo de fabricación de productos y una
clase abstracta (ProductBase) que la implementa parcialmente.
IProduct declara el contrato común (propiedades + firmas de métodos), mientras que ProductBase
proporciona el flujo compartido (método plantilla: Production()) y una implementación parcial.
MetalProduct, WoodProduct y PlasticProduct personalizan esta estructura.
Usamos un enumerador (SizeOption) para valores de tamaño coherentes como “S/M/L”,
y una interfaz de control de calidad simple IQualityCheck que los productos implementan.
using System;
using System.Collections.Generic;
// Enum para estandarizar los tamaños
public enum SizeOption
{
Small,
Medium,
Large
}
// Contrato común: propiedades y comportamientos que todos los productos deben tener
public interface IProduct
{
string Name { get; set; }
string SKU { get; set; }
SizeOption Size { get; set; }
decimal UnitCost { get; set; } // Costo unitario
int Stock { get; set; } // Unidades en stock
// Flujo de producción y cálculo de costos
void Production();
decimal CalculateCost(int quantity);
string GetSpecs();
}
// Contrato simple de control de calidad
public interface IQualityCheck
{
bool QualityCheck(out string reason);
}
// CLASE ABSTRACTA: Implementación parcial + flujo compartido (Patrón de Método Plantilla)
public abstract class ProductBase : IProduct, IQualityCheck
{
public string Name { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public SizeOption Size { get; set; }
public decimal UnitCost { get; set; }
public int Stock { get; set; }
// Factor basado en material (las subclases pueden sobrescribir)
protected virtual decimal MaterialFactor => 1.00m;
// Método plantilla: ejecuta los pasos de producción en un flujo fijo
public void Production()
{
LogStep($"Producción iniciada: {Name} ({SKU})");
PrepareMaterials();
Assemble();
Finish();
LogStep("Producción completada.\n");
}
// Cálculo de costo base (las subclases pueden sobrescribir si es necesario)
public virtual decimal CalculateCost(int quantity)
{
if (quantity <= 0) return 0m;
// Modelo simple: costo unitario * cantidad * factor material * gastos generales (10 %)
decimal baseCost = UnitCost * quantity * MaterialFactor;
decimal overhead = baseCost * 0.10m;
return baseCost + overhead;
}
// Especificación técnica común
public virtual string GetSpecs()
=> $"SKU: {SKU}, Tamaño: {Size}, Costo unitario: {UnitCost:0.00} EUR, Stock: {Stock}";
// Control de calidad por defecto: ¿son plausibles el costo y el stock?
public virtual bool QualityCheck(out string reason)
{
if (UnitCost <= 0) { reason = "El costo unitario no puede ser cero/negativo."; return false; }
if (Stock < 0) { reason = "El stock no puede ser negativo."; return false; }
reason = "OK";
return true;
}
// Pasos que las subclases deben implementar
protected abstract void PrepareMaterials();
protected abstract void Assemble();
protected virtual void Finish()
{
LogStep("Control de calidad general y empaquetado");
}
protected void LogStep(string message) => Console.WriteLine($" - {message}");
}
// PRODUCTO METÁLICO
public class MetalProduct : ProductBase
{
public string Alloy { get; set; } = "Acero 304L";
public bool RequiresHeatTreatment { get; set; } = true;
protected override decimal MaterialFactor => 1.45m; // el procesamiento de metal es más costoso
protected override void PrepareMaterials()
{
LogStep($"Preparando lámina/bloque de metal (Aleación: {Alloy})");
if (RequiresHeatTreatment) LogStep("Preparando horno para tratamiento térmico");
}
protected override void Assemble()
{
LogStep("Corte, doblado, soldadura y pulido de superficie");
if (RequiresHeatTreatment) LogStep("Aplicando tratamiento térmico (templado)");
}
protected override void Finish()
{
LogStep("Protección anticorrosión y recubrimiento de superficie");
base.Finish();
}
public override string GetSpecs()
=> $"[Metal] {Name} | {base.GetSpecs()} | Aleación: {Alloy} | Tratamiento térmico: {(RequiresHeatTreatment ? "Sí" : "No")}";
public override bool QualityCheck(out string reason)
{
if (!base.QualityCheck(out reason)) return false;
// Regla extra para metal: el nombre de la aleación no debe estar vacío
if (string.IsNullOrWhiteSpace(Alloy)) { reason = "La información de la aleación no puede estar vacía."; return false; }
return true;
}
}
// PRODUCTO DE MADERA
public class WoodProduct : ProductBase
{
public string WoodType { get; set; } = "Roble";
public bool IsVarnished { get; set; } = true;
protected override decimal MaterialFactor => 1.20m;
protected override void PrepareMaterials()
{
LogStep($"Selección y secado de la madera (Tipo: {WoodType})");
LogStep("Corte y lijado");
}
protected override void Assemble()
{
LogStep("Ensamblaje: espiga/espiga y pegamento");
}
protected override void Finish()
{
LogStep(IsVarnished ? "Aplicación de barniz" : "Aplicación de aceite/protector");
base.Finish();
}
public override string GetSpecs()
=> $"[Madera] {Name} | {base.GetSpecs()} | Tipo de madera: {WoodType} | Barniz: {(IsVarnished ? "Sí" : "No")}";
}
// PRODUCTO DE PLÁSTICO
public class PlasticProduct : ProductBase
{
public string Polymer { get; set; } = "PP";
public bool Recyclable { get; set; } = true;
protected override decimal MaterialFactor => 0.95m; // los plásticos suelen ser más baratos
protected override void PrepareMaterials()
{
LogStep($"Preparando materia prima granulada (Polímero: {Polymer})");
LogStep("Mezcla de masterbatch de color");
}
protected override void Assemble()
{
LogStep("Moldeo por inyección y desbarbado");
}
protected override void Finish()
{
LogStep("Inspección de superficie y etiquetado");
base.Finish();
}
public override string GetSpecs()
=> $"[Plástico] {Name} | {base.GetSpecs()} | Polímero: {Polymer} | Reciclable: {(Recyclable ? "Sí" : "No")}";
}
// DEMO
class Program
{
static void Main()
{
var items = new List<IProduct>
{
new MetalProduct
{
Name = "Estante de acero",
SKU = "M-SH-001",
Size = SizeOption.Large,
UnitCost = 500m,
Stock = 40,
Alloy = "304L",
RequiresHeatTreatment = true
},
new WoodProduct
{
Name = "Mesa de madera",
SKU = "W-TB-002",
Size = SizeOption.Medium,
UnitCost = 350m,
Stock = 12,
WoodType = "Roble",
IsVarnished = true
},
new PlasticProduct
{
Name = "Caja de plástico",
SKU = "P-CR-003",
Size = SizeOption.Small,
UnitCost = 80m,
Stock = 200,
Polymer = "PP",
Recyclable = true
}
};
const int qty = 10;
foreach (var p in items)
{
Console.WriteLine($"\n=== {p.Name} ===");
Console.WriteLine(p.GetSpecs());
p.Production();
Console.WriteLine($"Costo total ({qty} unidades): {p.CalculateCost(qty):0.00} EUR");
if (p is IQualityCheck qc)
{
Console.WriteLine(qc.QualityCheck(out var reason)
? "Control de calidad: OK"
: $"Control de calidad: ERROR - {reason}");
}
}
}
}
Resumen: La interfaz IProduct proporciona un contrato común para diferentes tipos de productos.
La clase abstracta ProductBase implementa parcialmente este contrato y estandariza el flujo de producción (método plantilla).
Las subclases (Metal/Madera/Plástico) personalizan los pasos específicos del material y las suposiciones de costos.
Al combinar interface + clase abstracta, se obtiene tanto flexibilidad como reutilización.
Ventajas
- Interface: Flexibilidad gracias a la implementación múltiple.
- Clase Abstracta: Reduce la duplicación de código con estado compartido e implementación parcial.
- Ambos: Reducen el acoplamiento y aumentan la capacidad de prueba.
TL;DR
abstract class: Define comportamiento común + requerido; puede incluir cuerpos de métodos.interface: Solo declara firmas; soporta implementación múltiple.- Una clase solo puede heredar de una clase abstracta/base pero puede implementar múltiples interfaces.
Artículos relacionados
Clases Sealed, Static y Partial en C#
Aprende el propósito, las diferencias y los casos de uso de las clases sealed, static y partial en C#.
Clases, Objetos, Propiedades y Métodos en C#
Aprende cómo las clases, objetos, propiedades y métodos en C# forman la base de la programación orientada a objetos.
Delegados y Eventos en C#
Aprende delegados y eventos en C# para crear aplicaciones basadas en eventos con callbacks y ejemplos prácticos.
Encapsulación, Herencia y Polimorfismo en C#
Aprende encapsulación, herencia y polimorfismo en C# con ejemplos claros para dominar los principios básicos de la POO.
Fundamentos de Inyección de Dependencias en C#
Aprende los fundamentos de Inyección de Dependencias en C#, gestionando dependencias y logrando bajo acoplamiento.
Métodos de Extensión en C#
Aprende métodos de extensión en C# para añadir nuevas funcionalidades a tipos existentes sin modificar su código.
Namespaces y ensamblados en C#
Aprende los conceptos de namespaces y ensamblados en C# para organizar el código y gestionar dependencias correctamente.
Principios SOLID en C#
Aplicación de los principios SOLID en C# con ejemplos: código más flexible, mantenible y comprobable.