CSharp

Tutorial CSharp: Interfaces

Aprende en C# qué son las interfaces, cómo implementarlas y sus diferencias con la herencia para un código modular y flexible.

Aprende CSharp y certifícate

Definición de interfaces

En C#, una interfaz es un contrato que define un conjunto de miembros (métodos, propiedades, eventos, etc.) que una clase debe implementar. A diferencia de las clases, las interfaces no contienen implementaciones de estos miembros, solo sus declaraciones. Podemos pensar en una interfaz como un "plano" que establece qué debe hacer una clase, pero no cómo debe hacerlo.

Las interfaces son fundamentales en la programación orientada a objetos en C# porque permiten definir comportamientos comunes que pueden ser implementados por diferentes clases, independientemente de su jerarquía de herencia. Esto facilita la creación de código más flexible y modular.

Sintaxis básica

Para definir una interfaz en C#, utilizamos la palabra clave interface seguida del nombre de la interfaz. Por convención, los nombres de interfaces comienzan con la letra "I" mayúscula:

interface INombre
{
    // Declaraciones de miembros
}

Dentro de la interfaz, podemos declarar:

  • Métodos: Funciones que las clases implementadoras deberán definir
  • Propiedades: Características que las clases deberán implementar
  • Eventos: Notificaciones que las clases podrán emitir
  • Indexadores: Permiten acceder a objetos como si fueran arrays

Ejemplo de definición de interfaz

Veamos un ejemplo sencillo de una interfaz que define el comportamiento de un vehículo:

interface IVehiculo
{
    // Propiedades (solo declaración)
    string Modelo { get; set; }
    int Año { get; set; }
    
    // Métodos (solo declaración)
    void Arrancar();
    void Detener();
    double CalcularConsumo(double distancia, double combustible);
}

En este ejemplo, la interfaz IVehiculo define:

  • Dos propiedades: Modelo y Año
  • Tres métodos: Arrancar(), Detener() y CalcularConsumo()

Cualquier clase que implemente esta interfaz deberá proporcionar una implementación para todos estos miembros.

Características importantes de las interfaces

Las interfaces en C# tienen varias características que es importante conocer:

  • No contienen implementaciones: Las interfaces solo definen la estructura, no el comportamiento.
  • No pueden contener campos: Las interfaces no pueden tener variables de instancia.
  • No pueden definir constructores: Ya que no representan objetos concretos.
  • Pueden heredar de otras interfaces: Una interfaz puede extender una o más interfaces.
  • Los miembros son implícitamente públicos: No es necesario (ni permitido) especificar modificadores de acceso.

Interfaces vacías

También es posible definir interfaces sin miembros, conocidas como interfaces marcadoras. Estas interfaces se utilizan principalmente para "marcar" clases con cierta característica:

interface IImprimible
{
    // Interfaz vacía que marca clases que pueden imprimirse
}

Aunque no contienen miembros, estas interfaces pueden ser útiles para identificar objetos con ciertas capacidades mediante el operador is o para restringir parámetros genéricos.

Interfaces con miembros estáticos

A partir de C# 8.0, las interfaces pueden contener miembros estáticos, incluyendo métodos, propiedades, campos y constructores estáticos:

interface IEjemplo
{
    // Miembro estático
    static void MetodoEstatico()
    {
        Console.WriteLine("Este es un método estático en una interfaz");
    }
}

Sin embargo, para un curso introductorio, es recomendable centrarse primero en el uso básico de interfaces antes de explorar estas características más avanzadas.

Convenciones de nomenclatura

En C#, existen convenciones de nomenclatura estándar para las interfaces:

  • Los nombres de interfaces comienzan con la letra "I" mayúscula
  • Se utilizan nombres descriptivos que reflejan la capacidad que proporciona la interfaz
  • Se prefieren nombres basados en adjetivos (como IComparable, IDisposable) o sustantivos que describan capacidades (ICollection, IList)

Seguir estas convenciones hace que el código sea más legible y comprensible para otros desarrolladores.

Ejemplo completo de definición de interfaz

Veamos un ejemplo más completo de una interfaz para un sistema de gestión de documentos:

interface IDocumento
{
    // Propiedades
    string Titulo { get; set; }
    string Autor { get; set; }
    DateTime FechaCreacion { get; }
    
    // Métodos
    void Guardar(string ruta);
    void Abrir(string ruta);
    string GenerarResumen();
    
    // Método con parámetros opcionales
    void Imprimir(bool enColor = true, int copias = 1);
}

Esta interfaz define el comportamiento que cualquier tipo de documento en nuestro sistema debería tener, independientemente de si es un documento de texto, una hoja de cálculo o una presentación.

Implementación básica

Una vez que hemos definido una interfaz, el siguiente paso es implementarla en una clase. Implementar una interfaz significa que la clase se compromete a proporcionar código real para todos los miembros declarados en dicha interfaz.

Para implementar una interfaz en C#, utilizamos dos puntos (:) después del nombre de la clase, seguido del nombre de la interfaz. Una clase puede implementar múltiples interfaces, separándolas por comas.

Sintaxis de implementación

La estructura básica para implementar una interfaz es la siguiente:

class NombreClase : INombreInterfaz
{
    // Implementación de todos los miembros de la interfaz
}

Veamos un ejemplo sencillo utilizando la interfaz IVehiculo que vimos anteriormente:

class Coche : IVehiculo
{
    // Implementación de las propiedades
    public string Modelo { get; set; }
    public int Año { get; set; }
    
    // Implementación de los métodos
    public void Arrancar()
    {
        Console.WriteLine("El coche está arrancando");
    }
    
    public void Detener()
    {
        Console.WriteLine("El coche se ha detenido");
    }
    
    public double CalcularConsumo(double distancia, double combustible)
    {
        return distancia / combustible;
    }
}

En este ejemplo, la clase Coche implementa la interfaz IVehiculo proporcionando código real para cada uno de los miembros declarados en la interfaz.

Reglas de implementación

Al implementar una interfaz, debes seguir estas reglas importantes:

  • Debes implementar todos los miembros definidos en la interfaz.
  • Los miembros implementados deben ser públicos.
  • Los nombres, tipos de retorno y parámetros deben coincidir exactamente con los definidos en la interfaz.
  • Puedes añadir miembros adicionales que no estén en la interfaz.

Implementación de múltiples interfaces

Una de las ventajas de las interfaces es que una clase puede implementar varias interfaces simultáneamente, lo que no es posible con la herencia de clases (que está limitada a una sola clase base):

interface IElectrico
{
    int NivelBateria { get; set; }
    void Cargar();
}

class CocheElectrico : IVehiculo, IElectrico
{
    // Implementación de IVehiculo
    public string Modelo { get; set; }
    public int Año { get; set; }
    
    public void Arrancar()
    {
        Console.WriteLine("El coche eléctrico está arrancando silenciosamente");
    }
    
    public void Detener()
    {
        Console.WriteLine("El coche eléctrico se ha detenido");
    }
    
    public double CalcularConsumo(double distancia, double energia)
    {
        return energia / distancia * 100; // kWh/100km
    }
    
    // Implementación de IElectrico
    public int NivelBateria { get; set; }
    
    public void Cargar()
    {
        Console.WriteLine("Cargando batería...");
        NivelBateria = 100;
    }
}

En este ejemplo, CocheElectrico implementa tanto IVehiculo como IElectrico, lo que significa que debe proporcionar implementaciones para todos los miembros de ambas interfaces.

Uso de interfaces implementadas

Una vez que una clase implementa una interfaz, podemos:

  1. Crear instancias de la clase y acceder a los miembros implementados:
CocheElectrico tesla = new CocheElectrico();
tesla.Modelo = "Model 3";
tesla.Año = 2023;
tesla.Arrancar();
tesla.Cargar();
  1. Usar la interfaz como tipo para variables o parámetros:
IVehiculo miVehiculo = new Coche();
miVehiculo.Arrancar();

// Podemos pasar cualquier objeto que implemente IVehiculo
void MostrarInfoVehiculo(IVehiculo vehiculo)
{
    Console.WriteLine($"Modelo: {vehiculo.Modelo}, Año: {vehiculo.Año}");
}

Este segundo punto es especialmente importante porque nos permite escribir código que trabaja con cualquier objeto que implemente la interfaz, sin importar su tipo concreto.

Implementación de interfaces con clases abstractas

También podemos implementar interfaces en clases abstractas. En este caso, la clase abstracta no necesita implementar todos los miembros de la interfaz, dejando algunos para que sean implementados por las clases derivadas:

abstract class VehiculoBase : IVehiculo
{
    public string Modelo { get; set; }
    public int Año { get; set; }
    
    // Implementación concreta
    public void Detener()
    {
        Console.WriteLine("Vehículo detenido");
    }
    
    // Método abstracto que deben implementar las clases derivadas
    public abstract void Arrancar();
    
    // Método abstracto que deben implementar las clases derivadas
    public abstract double CalcularConsumo(double distancia, double combustible);
}

class Motocicleta : VehiculoBase
{
    // Implementamos los métodos abstractos
    public override void Arrancar()
    {
        Console.WriteLine("La moto está arrancando");
    }
    
    public override double CalcularConsumo(double distancia, double combustible)
    {
        return distancia / combustible * 100; // L/100km
    }
}

Ejemplo práctico: Sistema de notificaciones

Veamos un ejemplo práctico de cómo las interfaces pueden ayudarnos a crear sistemas flexibles. Imaginemos un sistema de notificaciones que puede enviar mensajes por diferentes medios:

interface INotificador
{
    void EnviarNotificacion(string mensaje);
    bool PuedeEnviar { get; }
}

class NotificadorEmail : INotificador
{
    public bool PuedeEnviar { get; private set; } = true;
    
    public void EnviarNotificacion(string mensaje)
    {
        // Código para enviar email
        Console.WriteLine($"Enviando email: {mensaje}");
    }
}

class NotificadorSMS : INotificador
{
    public bool PuedeEnviar { get; private set; } = true;
    
    public void EnviarNotificacion(string mensaje)
    {
        // Código para enviar SMS
        Console.WriteLine($"Enviando SMS: {mensaje}");
    }
}

// Clase que usa los notificadores
class ServicioAlertas
{
    private List<INotificador> _notificadores = new List<INotificador>();
    
    public void AgregarNotificador(INotificador notificador)
    {
        _notificadores.Add(notificador);
    }
    
    public void EnviarAlerta(string mensaje)
    {
        foreach (var notificador in _notificadores)
        {
            if (notificador.PuedeEnviar)
            {
                notificador.EnviarNotificacion(mensaje);
            }
        }
    }
}

Y así podríamos usar este sistema:

// Crear el servicio de alertas
ServicioAlertas servicio = new ServicioAlertas();

// Agregar diferentes notificadores
servicio.AgregarNotificador(new NotificadorEmail());
servicio.AgregarNotificador(new NotificadorSMS());

// Enviar una alerta a través de todos los notificadores
servicio.EnviarAlerta("¡Sistema en mantenimiento!");

Este ejemplo muestra cómo las interfaces nos permiten desacoplar el sistema de alertas de los mecanismos específicos de notificación. Podemos añadir nuevos tipos de notificadores (como notificaciones push o mensajes en redes sociales) sin modificar el código del ServicioAlertas.

Implementación de interfaces heredadas

Cuando una interfaz hereda de otra, las clases que implementan la interfaz derivada deben implementar los miembros de ambas interfaces:

interface IBasico
{
    void MetodoBasico();
}

interface IAvanzado : IBasico
{
    void MetodoAvanzado();
}

class Implementacion : IAvanzado
{
    // Debe implementar métodos de IBasico
    public void MetodoBasico()
    {
        Console.WriteLine("Implementación del método básico");
    }
    
    // Y también métodos de IAvanzado
    public void MetodoAvanzado()
    {
        Console.WriteLine("Implementación del método avanzado");
    }
}

La implementación de interfaces es una herramienta fundamental en C# que nos permite crear código más flexible, modular y fácil de mantener, especialmente cuando necesitamos que diferentes clases compartan comportamientos comunes sin estar necesariamente relacionadas por herencia.

Interfaces vs herencia

En C#, tanto las interfaces como la herencia son mecanismos fundamentales de la programación orientada a objetos, pero sirven para propósitos diferentes y tienen distintas implicaciones en el diseño de nuestras aplicaciones. Entender cuándo usar cada una es crucial para crear código bien estructurado.

Diferencias fundamentales

La principal diferencia entre interfaces y herencia radica en lo que cada una representa:

  • Una interfaz define un contrato que especifica "qué" debe hacer una clase, sin indicar "cómo" debe hacerlo.
  • La herencia establece una relación "es un" entre clases, donde la clase derivada hereda comportamiento y estado de la clase base.

Veamos un ejemplo sencillo que ilustra esta diferencia:

// Usando herencia
public class Animal
{
    public string Nombre { get; set; }
    
    public virtual void HacerSonido()
    {
        Console.WriteLine("Algún sonido");
    }
}

public class Perro : Animal
{
    public override void HacerSonido()
    {
        Console.WriteLine("Guau guau");
    }
}

// Usando interfaces
public interface ISonoro
{
    void HacerSonido();
}

public class Telefono : ISonoro
{
    public void HacerSonido()
    {
        Console.WriteLine("Ring ring");
    }
}

En este ejemplo, Perro hereda de Animal porque un perro "es un" animal. Por otro lado, Telefono implementa ISonoro porque puede hacer sonidos, pero no "es un" animal.

Limitaciones de la herencia

La herencia en C# tiene algunas limitaciones importantes:

  • Herencia única: Una clase solo puede heredar de una única clase base, lo que puede ser restrictivo cuando necesitamos combinar comportamientos de múltiples fuentes.
// Esto NO es posible en C#
public class MiClase : ClaseBase1, ClaseBase2 // Error: herencia múltiple no permitida
{
}
  • Acoplamiento fuerte: La herencia crea un acoplamiento fuerte entre clases, lo que significa que cambios en la clase base pueden afectar a todas las clases derivadas.

  • Jerarquías rígidas: Las relaciones de herencia pueden volverse complicadas y difíciles de mantener a medida que la jerarquía crece.

Ventajas de las interfaces

Las interfaces ofrecen varias ventajas sobre la herencia en ciertos escenarios:

  • Implementación múltiple: Una clase puede implementar múltiples interfaces, lo que permite combinar diferentes comportamientos.
public interface IVolador
{
    void Volar();
}

public interface INadador
{
    void Nadar();
}

// Una clase puede implementar múltiples interfaces
public class Pato : Animal, IVolador, INadador
{
    public void Volar()
    {
        Console.WriteLine("El pato está volando");
    }
    
    public void Nadar()
    {
        Console.WriteLine("El pato está nadando");
    }
    
    public override void HacerSonido()
    {
        Console.WriteLine("Cuac cuac");
    }
}
  • Acoplamiento débil: Las interfaces promueven un acoplamiento más débil entre componentes, lo que facilita la sustitución de implementaciones.

  • Diseño por contrato: Las interfaces permiten definir claramente las capacidades que una clase debe proporcionar sin imponer una estructura de herencia.

Cuándo usar herencia

La herencia es más adecuada cuando:

  • Existe una clara relación "es un" entre clases.
  • Quieres reutilizar código (implementaciones) de la clase base.
  • Las clases derivadas comparten estado y comportamiento común.
public class Forma
{
    public double X { get; set; }
    public double Y { get; set; }
    
    public virtual double CalcularArea()
    {
        return 0;
    }
}

public class Circulo : Forma
{
    public double Radio { get; set; }
    
    public override double CalcularArea()
    {
        return Math.PI * Radio * Radio;
    }
}

En este ejemplo, Circulo hereda de Forma porque un círculo "es una" forma y comparte propiedades como posición (X, Y).

Cuándo usar interfaces

Las interfaces son preferibles cuando:

  • Necesitas definir capacidades que pueden ser implementadas por clases no relacionadas.
  • Quieres permitir que una clase participe en múltiples comportamientos.
  • Deseas desacoplar el sistema de implementaciones específicas.
public interface IGuardable
{
    void Guardar(string ruta);
    void Cargar(string ruta);
}

// Clases no relacionadas pueden implementar la misma interfaz
public class Documento : IGuardable
{
    public string Contenido { get; set; }
    
    public void Guardar(string ruta)
    {
        // Guardar contenido en un archivo
        File.WriteAllText(ruta, Contenido);
    }
    
    public void Cargar(string ruta)
    {
        // Cargar contenido desde un archivo
        Contenido = File.ReadAllText(ruta);
    }
}

public class ConfiguracionAplicacion : IGuardable
{
    public Dictionary<string, string> Ajustes { get; set; }
    
    public void Guardar(string ruta)
    {
        // Código para serializar y guardar ajustes
    }
    
    public void Cargar(string ruta)
    {
        // Código para cargar y deserializar ajustes
    }
}

Combinando interfaces y herencia

En muchos casos, el mejor diseño implica combinar interfaces y herencia:

// Clase base abstracta
public abstract class Empleado
{
    public string Nombre { get; set; }
    public string Apellido { get; set; }
    public abstract double CalcularSalario();
}

// Interfaces para capacidades específicas
public interface IGeneraInforme
{
    string GenerarInforme();
}

public interface IGestionaEquipo
{
    void AsignarTarea(Empleado empleado, string tarea);
}

// Clases que combinan herencia e interfaces
public class Gerente : Empleado, IGeneraInforme, IGestionaEquipo
{
    public List<Empleado> Equipo { get; set; } = new List<Empleado>();
    
    public override double CalcularSalario()
    {
        return 5000 + (Equipo.Count * 500);
    }
    
    public string GenerarInforme()
    {
        return $"Informe del gerente {Nombre} {Apellido}";
    }
    
    public void AsignarTarea(Empleado empleado, string tarea)
    {
        Console.WriteLine($"Tarea '{tarea}' asignada a {empleado.Nombre}");
    }
}

public class Desarrollador : Empleado, IGeneraInforme
{
    public string Lenguaje { get; set; }
    
    public override double CalcularSalario()
    {
        return 3000 + (Lenguaje == "C#" ? 1000 : 500);
    }
    
    public string GenerarInforme()
    {
        return $"Informe técnico de {Nombre} sobre {Lenguaje}";
    }
}

En este ejemplo:

  • Usamos herencia para modelar la relación "es un" entre Empleado y sus tipos específicos.
  • Usamos interfaces para definir capacidades adicionales como generar informes o gestionar equipos.

Principio de sustitución de Liskov

Un concepto importante al decidir entre interfaces y herencia es el Principio de Sustitución de Liskov, que establece que los objetos de una clase derivada deben poder sustituir a los objetos de la clase base sin afectar la corrección del programa.

Si no se puede cumplir este principio, es mejor usar interfaces en lugar de herencia:

// Mal uso de herencia
public class Rectangulo
{
    public virtual int Ancho { get; set; }
    public virtual int Alto { get; set; }
    
    public int CalcularArea()
    {
        return Ancho * Alto;
    }
}

public class Cuadrado : Rectangulo
{
    // Esto viola el principio de sustitución de Liskov
    public override int Ancho
    {
        get => base.Ancho;
        set
        {
            base.Ancho = value;
            base.Alto = value;  // Un cuadrado debe tener lados iguales
        }
    }
    
    public override int Alto
    {
        get => base.Alto;
        set
        {
            base.Alto = value;
            base.Ancho = value;  // Un cuadrado debe tener lados iguales
        }
    }
}

// Mejor enfoque con interfaces
public interface IForma
{
    int CalcularArea();
}

public class Rectangulo : IForma
{
    public int Ancho { get; set; }
    public int Alto { get; set; }
    
    public int CalcularArea()
    {
        return Ancho * Alto;
    }
}

public class Cuadrado : IForma
{
    public int Lado { get; set; }
    
    public int CalcularArea()
    {
        return Lado * Lado;
    }
}

Resumen comparativo

Característica Herencia Interfaces
Relación "Es un" "Puede hacer"
Implementación Proporciona implementación Solo define contrato
Múltiples fuentes No (herencia única) Sí (implementación múltiple)
Acoplamiento Fuerte Débil
Reutilización de código No
Flexibilidad Menor Mayor
Estado Puede heredar estado No incluye estado

La elección entre interfaces y herencia no es excluyente. Un buen diseño orientado a objetos a menudo utiliza ambos mecanismos de forma complementaria para crear sistemas flexibles, mantenibles y extensibles.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende CSharp online

Ejercicios de esta lección Interfaces

Evalúa tus conocimientos de esta lección Interfaces con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender qué es una interfaz y su propósito en C#.
  • Aprender a definir e implementar interfaces en clases y clases abstractas.
  • Conocer las reglas y convenciones para el uso de interfaces.
  • Diferenciar entre interfaces y herencia, y cuándo usar cada una.
  • Aplicar interfaces para crear sistemas desacoplados y flexibles.