1. Interfaces en la lógica de negocio: botones y acciones
Esta es la última lección sobre interfaces, así que aquí habrá muchos ejemplos prácticos para que pilles mejor cómo se usan las interfaces en la vida real.
Las interfaces son especialmente valiosas en aplicaciones grandes. Por eso, aquí tienes casos más complejos, a veces adelantándonos un poco al temario. Si te mola — ¡genial! Si no — pasa al siguiente nivel :P
El ejemplo más sencillo — botones y clics
Vamos a seguir desarrollando nuestra app de consola de aprendizaje — imagina que tenemos un sistema sencillo de elementos de usuario (por ejemplo, botones, campos de texto y selectores) en un menú de consola.
Queremos que algunos elementos reaccionen al click (botones), y otros no (por ejemplo, un título estático). Las interfaces nos permiten implementar esto de forma elegante.
Paso 1: Creamos la interfaz
// Componente que se puede "clicar":
public interface IClickable
{
void Click();
}
Paso 2: Usamos la interfaz para distintos tipos de elementos
// Clase base para todos los elementos del menú
public class MenuItem
{
public string Title { get; set; }
public MenuItem(string title)
{
Title = title;
}
public virtual void Display()
{
Console.WriteLine(Title);
}
}
// Botón: se puede "clicar"
public class Button : MenuItem, IClickable
{
public Button(string title) : base(title) {}
public void Click()
{
Console.WriteLine($"[Botón] {Title} fue pulsado!");
}
}
// Solo texto: no se puede clicar
public class Label : MenuItem
{
public Label(string title) : base(title) {}
}
Paso 3: Usamos polimorfismo de interfaz
IClickable[] clickableItems = new IClickable[]
{
new Button("Guardar"),
new Button("Salir")
// Label aquí no se puede añadir — ¡no es IClickable!
};
foreach (var item in clickableItems)
{
item.Click();
}
¿Ves la magia? La lista solo acepta a los que “saben” Click, así que no tendrás error en tiempo de ejecución si intentas “clicar” un objeto que no toca.
2. Interfaces y el patrón “Estrategia”: elegir algoritmo al vuelo
Supón que estás haciendo una app donde puedes guardar informes de distintas formas: en un archivo, en una base de datos, en “la nube”, o incluso en Telegram a tu jefe (no preguntes, cosas que pasan).
Paso 1: Planteamos el problema
Necesitamos que la clase ReportGenerator pueda trabajar con cualquier forma de guardar, sin saber los detalles de la implementación.
Paso 2: Definimos la interfaz-estrategia
public interface IDataSaver
{
void Save(string reportData);
}
Paso 3: Implementamos distintas variantes
public class FileDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[FileDataSaver] Guardando en archivo...\n" + reportData);
// Aquí podría ir el código para File.WriteAllText(...)
}
}
public class DatabaseDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[DatabaseDataSaver] Guardando en base de datos...\n" + reportData);
// Aquí podría ir el código para guardar en la base de datos
}
}
Paso 4: Usamos la interfaz para cambiar la estrategia de guardado
public class ReportGenerator
{
private readonly IDataSaver _dataSaver;
public ReportGenerator(IDataSaver dataSaver)
{
_dataSaver = dataSaver;
}
public void GenerateReport()
{
string report = "¡Este es un informe importante!";
Console.WriteLine("Generando informe...");
_dataSaver.Save(report);
}
}
Demostración de flexibilidad
// ¡Puedes cambiar la forma de guardar sin reescribir ReportGenerator!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();
IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();
Sentido práctico: Si mañana quieres guardar informes en un nuevo servicio supermoderno, solo tienes que escribir una nueva clase que implemente la interfaz y meter el objeto en tu arquitectura, ¡sin tocar ni una línea del código existente!
3. Interfaces en .NET: IDisposable y using
Una de las interfaces más usadas en .NET es IDisposable. La implementan todas las clases que trabajan con recursos no gestionados: archivos, streams, conexiones de red.
¿Para qué sirve IDisposable?
Cuando trabajas con un recurso que hay que liberar sí o sí (por ejemplo, cerrar un archivo), implementas IDisposable y escribes el método Dispose. Así puedes pasar estos objetos al operador using, que garantiza que se llame a Dispose al salir del bloque.
Ejemplo: Simulación de trabajo con archivo
public class FakeFile : IDisposable
{
public string FileName { get; }
public FakeFile(string fileName)
{
FileName = fileName;
Console.WriteLine($"Archivo abierto: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"Archivo cerrado: {FileName}");
}
}
// En el programa principal:
using (var file = new FakeFile("informe.txt"))
{
Console.WriteLine("Escribiendo en el archivo...");
// el archivo se "cerrará" automáticamente después del using
}
Salida:
Archivo abierto: informe.txt
Escribiendo en el archivo...
Archivo cerrado: informe.txt
Realmente útil: no se te olvidará cerrar el archivo — la interfaz y la infraestructura del lenguaje te cubren.
4. Interfaces en colecciones y LINQ
Cuando trabajas con listas, arrays, diccionarios, ya estás usando interfaces aunque no lo pienses.
List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // ¡todo ok!
// Ahora puedes recorrer los elementos así:
foreach(var x in enumerable)
{
Console.WriteLine(x);
}
La mayoría de los métodos LINQ funcionan con colecciones a través de la interfaz IEnumerable<T>. Esto te permite escribir código que no depende del tipo concreto de colección.
¿Para qué sirve esto?
Puedes cambiar List<T> por T[], HashSet<T> o incluso tu propia colección — ¡y tu código seguirá funcionando!
5. Interfaces para testing (objetos Mock)
En los tests es superimportante aislar el código de dependencias externas: no tocar la base real, ni archivos reales. Las interfaces permiten usar mocks y testear el código sin tocar bases o archivos reales.
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("¡Datos guardados en el mock!");
}
}
// En el test
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();
Console.WriteLine($"¿Se llamó a Save? {saver.WasCalled}");
Resultado: el test no depende del mundo real, ¡pero comprueba que se llamó al método necesario!
6. Implementación explícita de interfaces para resolver conflictos
A veces una clase tiene que implementar dos interfaces con métodos iguales, pero la lógica de esos métodos es distinta.
public interface IFlyable
{
void Move();
}
public interface ISwimmable
{
void Move();
}
public class Duck : IFlyable, ISwimmable
{
// Implementación explícita
void IFlyable.Move()
{
Console.WriteLine("¡El pato vuela!");
}
void ISwimmable.Move()
{
Console.WriteLine("¡El pato nada!");
}
}
Duck duck = new Duck();
// duck.Move(); // Error: ¡no existe ese método!
((IFlyable)duck).Move(); // "¡El pato vuela!"
((ISwimmable)duck).Move(); // "¡El pato nada!"
Puede parecer un poco hardcore, pero a veces ¡es justo lo que piden las especificaciones!
7. Interfaces en el modelo de eventos: INotifyPropertyChanged
En .NET hay interfaces estándar para soportar eventos — por ejemplo, cuando en los modelos de datos cambia algo y la UI tiene que enterarse (muy típico en WPF, MAUI y otros GUI).
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
Sentido: cualquier framework que soporte data binding espera que tu clase implemente esta interfaz — y así la UI se actualizará automáticamente cuando cambien las propiedades.
8. Interfaces en el patrón “Fábrica” (Factory)
Las interfaces van genial para construir fábricas — clases que crean objetos distintos pero intercambiables.
public interface ITransport
{
void Move();
}
public class Bicycle : ITransport
{
public void Move() => Console.WriteLine("¡Vamos en bici!");
}
public class Car : ITransport
{
public void Move() => Console.WriteLine("¡Vamos en coche!");
}
public class TransportFactory
{
public static ITransport Create(string type)
{
return type switch
{
"bike" => new Bicycle(),
"car" => new Car(),
_ => throw new ArgumentException("Tipo de transporte desconocido")
};
}
}
ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "¡Vamos en bici!"
9. Interfaces para eventos: ejemplo de evento propio
Vamos a definir una interfaz de “escuchador de eventos”:
public interface ILoginListener
{
void OnLogin(string userName);
}
// Clase que lanza el evento
public class LoginManager
{
private List<ILoginListener> listeners = new();
public void Subscribe(ILoginListener listener) => listeners.Add(listener);
public void Login(string userName)
{
Console.WriteLine($"Usuario {userName} ha entrado.");
foreach (var listener in listeners)
listener.OnLogin(userName);
}
}
// Clase que implementa la interfaz
public class WelcomeMessage : ILoginListener
{
public void OnLogin(string userName)
{
Console.WriteLine($"¡Bienvenido, {userName}!");
}
}
LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("John");
// Usuario John ha entrado.
// ¡Bienvenido, John!
En una entrevista: si te preguntan — “¿cómo harías tu propio sistema de eventos?”, cuenta sin miedo lo de las interfaces-listener.
10. Interfaces para plugins (extensibilidad de aplicaciones)
Muchas aplicaciones grandes soportan plugins. Gracias a las interfaces, tu app puede “cargar” nuevos módulos al vuelo sin saber nada de su interior.
public interface IPlugin
{
string Name { get; }
void Run();
}
// Tu aplicación:
public class PluginLoader
{
public void LoadAndRun(IEnumerable<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine($"Ejecutando plugin: {plugin.Name}");
plugin.Run();
}
}
}
Los plugins pueden ser desarrollados por terceros — lo importante es que implementen la interfaz. ¡Tu app se vuelve ampliable!
GO TO FULL VERSION