Principios SOLID aplicados en .NET Core con ejemplos

20 años de experiencia como desarrollador en tecnologías .NET. Desde que comencé mi aventura profesional no he dejado de interesarme por todo lo que rodea a esta tecnología. Me considero un apasionado de mi trabajo, intentando siempre aprender, evolucionar y conseguir unas metas y objetivos. La tecnología cambia constantemente y por ello es necesario tener una base consolidada y seguir adquiriendo nuevos y mayores conocimientos que hagan de nuestro trabajo más fácil. Intento siempre, aprender nuevas herramientas y funcionalidades relacionadas con la tecnología .NET que me ayude a seguir avanzando en mi carrera profesional y aportando nuevas ideas en los proyectos en los que participo.
Los principios SOLID son cinco principios fundamentales del diseño de software orientado a objetos que nos ayudan a crear código más mantenible, escalable y robusto. En este artículo exploraremos cada uno de estos principios aplicados específicamente en .NET Core con ejemplos prácticos.
📋 ¿Qué son los principios SOLID?
Los principios SOLID fueron introducidos por Robert C. Martin y representan un acrónimo de cinco principios de diseño:
- S - Single Responsibility Principle (Principio de Responsabilidad Única)
- O - Open/Closed Principle (Principio Abierto/Cerrado)
- L - Liskov Substitution Principle (Principio de Sustitución de Liskov)
- I - Interface Segregation Principle (Principio de Segregación de Interfaces)
- D - Dependency Inversion Principle (Principio de Inversión de Dependencias)
1️⃣ Single Responsibility Principle (SRP)
🎯 Definición
Una clase debe tener una sola razón para cambiar, es decir, debe tener una única responsabilidad.
❌ Ejemplo incorrecto
public class UserService
{
public void CreateUser(User user)
{
// Validación
if (string.IsNullOrEmpty(user.Email))
throw new ArgumentException("Email requerido");
// Guardar en base de datos
using var connection = new SqlConnection("connectionString");
connection.Execute("INSERT INTO Users...", user);
// Enviar email de bienvenida
var smtpClient = new SmtpClient("smtp.gmail.com");
smtpClient.Send(new MailMessage("admin@site.com", user.Email, "Bienvenido", "Mensaje"));
// Logging
Console.WriteLine($"Usuario {user.Email} creado exitosamente");
}
}
✅ Ejemplo correcto
public class UserValidator
{
public void Validate(User user)
{
if (string.IsNullOrEmpty(user.Email))
throw new ArgumentException("Email requerido");
}
}
public class UserRepository
{
public void Save(User user)
{
using var connection = new SqlConnection("connectionString");
connection.Execute("INSERT INTO Users...", user);
}
}
public class EmailService
{
public void SendWelcomeEmail(User user)
{
var smtpClient = new SmtpClient("smtp.gmail.com");
smtpClient.Send(new MailMessage("admin@site.com", user.Email, "Bienvenido", "Mensaje"));
}
}
public class UserService
{
private readonly UserValidator _validator;
private readonly UserRepository _repository;
private readonly EmailService _emailService;
private readonly ILogger<UserService> _logger;
public UserService(UserValidator validator, UserRepository repository,
EmailService emailService, ILogger<UserService> logger)
{
_validator = validator;
_repository = repository;
_emailService = emailService;
_logger = logger;
}
public void CreateUser(User user)
{
_validator.Validate(user);
_repository.Save(user);
_emailService.SendWelcomeEmail(user);
_logger.LogInformation($"Usuario {user.Email} creado exitosamente");
}
}
2️⃣ Open/Closed Principle (OCP)
🎯 Definición
Las entidades de software deben estar abiertas para extensión pero cerradas para modificación.
❌ Ejemplo incorrecto
public class PriceCalculator
{
public decimal Calculate(Product product, string customerType)
{
switch (customerType)
{
case "Regular":
return product.Price;
case "Premium":
return product.Price * 0.9m;
case "VIP":
return product.Price * 0.8m;
default:
return product.Price;
}
}
}
✅ Ejemplo correcto
public abstract class PricingStrategy
{
public abstract decimal Calculate(Product product);
}
public class RegularPricing : PricingStrategy
{
public override decimal Calculate(Product product)
{
return product.Price;
}
}
public class PremiumPricing : PricingStrategy
{
public override decimal Calculate(Product product)
{
return product.Price * 0.9m;
}
}
public class VIPPricing : PricingStrategy
{
public override decimal Calculate(Product product)
{
return product.Price * 0.8m;
}
}
public class PriceCalculator
{
public decimal Calculate(Product product, PricingStrategy strategy)
{
return strategy.Calculate(product);
}
}
3️⃣ Liskov Substitution Principle (LSP)
🎯 Definición
Los objetos de una clase derivada deben poder reemplazar a los objetos de la clase base sin alterar el funcionamiento del programa.
❌ Ejemplo incorrecto
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("El pájaro está volando");
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Los pingüinos no pueden volar");
}
}
✅ Ejemplo correcto
public abstract class Bird
{
public abstract void Move();
}
public class FlyingBird : Bird
{
public override void Move()
{
Fly();
}
protected virtual void Fly()
{
Console.WriteLine("El pájaro está volando");
}
}
public class WalkingBird : Bird
{
public override void Move()
{
Walk();
}
protected virtual void Walk()
{
Console.WriteLine("El pájaro está caminando");
}
}
public class Eagle : FlyingBird
{
protected override void Fly()
{
Console.WriteLine("El águila está volando alto");
}
}
public class Penguin : WalkingBird
{
protected override void Walk()
{
Console.WriteLine("El pingüino está caminando en el hielo");
}
}
4️⃣ Interface Segregation Principle (ISP)
🎯 Definición
Los clientes no deben ser forzados a depender de interfaces que no usan.
❌ Ejemplo incorrecto
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class Human : IWorker
{
public void Work() => Console.WriteLine("Humano trabajando");
public void Eat() => Console.WriteLine("Humano comiendo");
public void Sleep() => Console.WriteLine("Humano durmiendo");
}
public class Robot : IWorker
{
public void Work() => Console.WriteLine("Robot trabajando");
public void Eat() => throw new NotSupportedException("Los robots no comen");
public void Sleep() => throw new NotSupportedException("Los robots no duermen");
}
✅ Ejemplo correcto
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class Human : IWorkable, IEatable, ISleepable
{
public void Work() => Console.WriteLine("Humano trabajando");
public void Eat() => Console.WriteLine("Humano comiendo");
public void Sleep() => Console.WriteLine("Humano durmiendo");
}
public class Robot : IWorkable
{
public void Work() => Console.WriteLine("Robot trabajando");
}
5️⃣ Dependency Inversion Principle (DIP)
🎯 Definición
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
❌ Ejemplo incorrecto
public class OrderService
{
private readonly SqlOrderRepository _repository;
private readonly SmtpEmailService _emailService;
public OrderService()
{
_repository = new SqlOrderRepository();
_emailService = new SmtpEmailService();
}
public void ProcessOrder(Order order)
{
_repository.Save(order);
_emailService.SendConfirmation(order);
}
}
✅ Ejemplo correcto
public interface IOrderRepository
{
void Save(Order order);
}
public interface IEmailService
{
void SendConfirmation(Order order);
}
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
// Implementación para SQL Server
}
}
public class SmtpEmailService : IEmailService
{
public void SendConfirmation(Order order)
{
// Implementación SMTP
}
}
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
public OrderService(IOrderRepository repository, IEmailService emailService)
{
_repository = repository;
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
_repository.Save(order);
_emailService.SendConfirmation(order);
}
}
🔧 Configuración en .NET Core
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Registro de dependencias
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddScoped<OrderService>();
var app = builder.Build();
🎯 Ejemplo práctico completo: Sistema de Notificaciones
Veamos un ejemplo que integra todos los principios SOLID:
// Abstracciones
public interface INotificationChannel
{
Task SendAsync(string message, string recipient);
}
public interface INotificationService
{
Task SendNotificationAsync(string message, string recipient, NotificationType type);
}
public interface INotificationFactory
{
INotificationChannel CreateChannel(NotificationType type);
}
// Implementaciones concretas
public class EmailNotificationChannel : INotificationChannel
{
public async Task SendAsync(string message, string recipient)
{
// Lógica para envío de email
await Task.Delay(100);
Console.WriteLine($"Email enviado a {recipient}: {message}");
}
}
public class SmsNotificationChannel : INotificationChannel
{
public async Task SendAsync(string message, string recipient)
{
// Lógica para envío de SMS
await Task.Delay(50);
Console.WriteLine($"SMS enviado a {recipient}: {message}");
}
}
public class PushNotificationChannel : INotificationChannel
{
public async Task SendAsync(string message, string recipient)
{
// Lógica para push notification
await Task.Delay(30);
Console.WriteLine($"Push enviado a {recipient}: {message}");
}
}
// Factory para crear canales
public class NotificationFactory : INotificationFactory
{
private readonly IServiceProvider _serviceProvider;
public NotificationFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public INotificationChannel CreateChannel(NotificationType type)
{
return type switch
{
NotificationType.Email => _serviceProvider.GetRequiredService<EmailNotificationChannel>(),
NotificationType.Sms => _serviceProvider.GetRequiredService<SmsNotificationChannel>(),
NotificationType.Push => _serviceProvider.GetRequiredService<PushNotificationChannel>(),
_ => throw new ArgumentException($"Tipo de notificación no soportado: {type}")
};
}
}
// Servicio principal
public class NotificationService : INotificationService
{
private readonly INotificationFactory _factory;
public NotificationService(INotificationFactory factory)
{
_factory = factory;
}
public async Task SendNotificationAsync(string message, string recipient, NotificationType type)
{
var channel = _factory.CreateChannel(type);
await channel.SendAsync(message, recipient);
}
}
public enum NotificationType
{
Email,
Sms,
Push
}
🔧 Configuración en Program.cs
var builder = WebApplication.CreateBuilder(args);
// Registro de servicios siguiendo DIP
builder.Services.AddScoped<EmailNotificationChannel>();
builder.Services.AddScoped<SmsNotificationChannel>();
builder.Services.AddScoped<PushNotificationChannel>();
builder.Services.AddScoped<INotificationFactory, NotificationFactory>();
builder.Services.AddScoped<INotificationService, NotificationService>();
var app = builder.Build();
📊 Beneficios de aplicar SOLID
🔄 Mantenibilidad
El código es más fácil de mantener y modificar sin afectar otras partes del sistema.
🧪 Testabilidad
Las dependencias pueden ser fácilmente mockeadas para pruebas unitarias.
🔧 Flexibilidad
Nuevas funcionalidades pueden agregarse sin modificar código existente.
📈 Escalabilidad
El sistema puede crecer de manera ordenada y controlada.
🎉 Conclusión
Los principios SOLID son fundamentales para crear aplicaciones robustas y mantenibles en .NET Core. Al aplicarlos correctamente, obtenemos código más limpio, testeable y flexible. La inversión de dependencias nativa de .NET Core facilita enormemente la implementación de estos principios.
Recuerda que estos principios son guías, no reglas absolutas. Úsalos con criterio y adapta su aplicación según las necesidades específicas de tu proyecto.




