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ícateDefinició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
yAño
- Tres métodos:
Arrancar()
,Detener()
yCalcularConsumo()
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:
- 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();
- 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 | Sí | 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.
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.
CRUD en C# de modelo Customer sobre una lista
Arrays y listas
Objetos
Excepciones
Eventos
Lambdas
Diccionarios en C#
Variables y constantes
Tipos de datos
Herencia
Operadores
Uso de consultas LINQ
Clases y encapsulación
Uso de consultas LINQ
Excepciones
Control de flujo
Eventos
Diccionarios
Tipos de datos
Conjuntos, colas y pilas
Lambdas
Conjuntos, colas y pilas
Uso de async y await
Tareas
Constructores y destructores
Operadores
Arrays y listas
Polimorfismo
Polimorfismo
Variables y constantes
Proyecto colecciones y LINQ en C#
Clases y encapsulación
Creación de proyecto C#
Uso de async y await
Funciones
Delegados
Delegados
Constructores y destructores
Objetos
Control de flujo
Funciones
Tareas
Proyecto sintaxis en C#
Herencia C Sharp
OOP en C Sharp
Diccionarios
Introducción a C#
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
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.