Cargando...

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:


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


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