Skip to main content

Command Palette

Search for a command to run...

Principios SOLID aplicados en .NET Core con ejemplos

Published
7 min readView as Markdown
Principios SOLID aplicados en .NET Core con ejemplos
I

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.

More from this blog

ipeinado.dev

14 posts