Patrones de diseño en C#: guía completa con ejemplos
Los patrones de diseño (design patterns) son soluciones probadas para problemas típicos en el desarrollo de software. En el ecosistema de C# y .NET, ayudan a crear una arquitectura flexible, escalable y mantenible. En este artículo analizaremos cuatro patrones clave: Singleton, Factory Method, Observer y Strategy, con ejemplos reales en C#.
¿Por qué son necesarios los patrones de diseño en C#?
Los patrones no son bibliotecas o frameworks listos para usar. Son plantillas conceptuales que:
- Simplifican la comunicación — los desarrolladores utilizan una terminología unificada
- Mejoran la calidad del código — reducen el acoplamiento y aumentan la flexibilidad
- Ahorran tiempo — no es necesario reinventar la rueda
- Cumplen con los principios SOLID — la mayoría de los patrones implementan estos principios
1. Singleton (Instancia única)
Propósito: Garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a ella.
Cuándo usarlo:
- Registradores (loggers)
- Gestores de configuración
- Pool de conexiones a bases de datos
- Cachés
Ejemplo en C# (implementación thread-safe):
public sealed class ConfigurationManager{ private static readonly Lazy<ConfigurationManager> _instance = new Lazy<ConfigurationManager>(() => new ConfigurationManager());
private readonly Dictionary<string, string> _settings;
private ConfigurationManager() { _settings = new Dictionary<string, string> { { "ConnectionString", "Server=localhost;Database=myDb" }, { "ApiKey", "12345-ABCDE" } }; }
public static ConfigurationManager Instance => _instance.Value;
public string GetSetting(string key) { return _settings.TryGetValue(key, out var value) ? value : null; }}
// Usovar config = ConfigurationManager.Instance;Console.WriteLine(config.GetSetting("ConnectionString"));Ventajas:
- Acceso controlado a la única instancia
- Inicialización diferida (Lazy<T>)
- Seguridad para hilos sin bloqueos
2. Factory Method (Método de fábrica)
Propósito: Define una interfaz para crear un objeto, pero permite que las subclases alteren el tipo de objeto que se crea.
Cuándo usarlo:
- Cuando no se conoce de antemano el tipo concreto del objeto
- Cuando la creación del objeto requiere lógica compleja
- Para separar el código de creación del código de uso
Ejemplo en C#:
// Producto abstractopublic interface ILogger{ void Log(string message);}
// Productos concretospublic class FileLogger : ILogger{ public void Log(string message) => File.AppendAllText("log.txt", $"{DateTime.Now}: {message}");}
public class ConsoleLogger : ILogger{ public void Log(string message) => Console.WriteLine($"{DateTime.Now}: {message}");}
// Creador con método de fábricapublic abstract class LoggerFactory{ public abstract ILogger CreateLogger(); public void LogMessage(string message) { var logger = CreateLogger(); logger.Log(message); }}
public class FileLoggerFactory : LoggerFactory{ public override ILogger CreateLogger() => new FileLogger();}
public class ConsoleLoggerFactory : LoggerFactory{ public override ILogger CreateLogger() => new ConsoleLogger();}
// UsoLoggerFactory factory = new ConsoleLoggerFactory();factory.LogMessage("¡Hola, mundo!");