Principios Arquitectónicos 📐
Los principios arquitectónicos son las "leyes" que guían buen diseño de software. SOLID es el conjunto más importante.
SOLID Principles
1. S — Single Responsibility Principle (SRP)
Una clase debe tener una única razón para cambiar.
// ❌ VIOLACIÓN: OrderService hace demasiadas cosas
public class OrderService
{
public void ProcessOrder(Order order)
{
// Validación
if (order.Total < 0) throw new Exception("Invalid total");
// Persistencia
var connection = new SqlConnection("...");
var command = connection.CreateCommand();
command.CommandText = "INSERT INTO Orders ...";
command.ExecuteNonQuery();
// Email
var smtp = new SmtpClient("smtp.gmail.com");
var message = new MailMessage("...", order.CustomerEmail);
message.Body = "Order confirmed!";
smtp.Send(message);
// Logging
var logFile = File.AppendText("log.txt");
logFile.WriteLine($"Order {order.Id} processed");
logFile.Close();
}
}
// Cambios en: validación, DB, email, logging → afectan OrderService
// ✅ CORRECTO: Separar responsabilidades
public class OrderService // Responsabilidad: Lógica de órdenes
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly ILogger _logger;
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
order.Validate(); // Delegado a Order (SRP)
await _repository.SaveAsync(order);
await _emailService.SendConfirmationAsync(order);
_logger.LogOrderProcessed(order.Id);
return new OrderResult { Success = true, OrderId = order.Id };
}
}
public class Order // Responsabilidad: Validar orden
{
public void Validate()
{
if (Total < 0) throw new ArgumentException("Invalid total");
if (CustomerId == Guid.Empty) throw new ArgumentException("Invalid customer");
}
}
public interface IOrderRepository // Responsabilidad: Persistencia
{
Task SaveAsync(Order order);
}
public interface IEmailService // Responsabilidad: Enviar emails
{
Task SendConfirmationAsync(Order order);
}
public interface ILogger // Responsabilidad: Logging
{
void LogOrderProcessed(Guid orderId);
}
Beneficios:
- Cada clase tiene una razón clara para cambiar
- Fácil de testear
- Reutilizable
- Mantenible
2. O — Open/Closed Principle (OCP)
Las clases deben estar abiertas para extensión pero cerradas para modificación.
// ❌ VIOLACIÓN: Modificar clase para agregar nuevos tipos
public class OrderProcessor
{
public decimal CalculateDiscount(Order order)
{
// Cada vez que agrega tipo de cliente, modifica este método
if (order.CustomerType == "Premium")
return order.Total * 0.20m;
else if (order.CustomerType == "Gold")
return order.Total * 0.15m;
else if (order.CustomerType == "Regular")
return order.Total * 0.05m;
else
throw new Exception("Unknown customer type");
}
}
// ✅ CORRECTO: Usar polimorfismo (Strategy pattern)
public interface IDiscountStrategy
{
decimal CalculateDiscount(decimal total);
}
public class PremiumDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal total) => total * 0.20m;
}
public class GoldDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal total) => total * 0.15m;
}
public class RegularDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal total) => total * 0.05m;
}
public class OrderProcessor
{
public decimal CalculateDiscount(Order order, IDiscountStrategy strategy)
{
return strategy.CalculateDiscount(order.Total);
// Nueva estrategia = nueva clase, NO modificar OrderProcessor
}
}
// Uso
var strategy = order.CustomerType switch
{
"Premium" => new PremiumDiscountStrategy(),
"Gold" => new GoldDiscountStrategy(),
_ => new RegularDiscountStrategy()
};
var discount = processor.CalculateDiscount(order, strategy);
Beneficios:
- Extensible sin riesgo de romper código existente
- Nuevas funcionalidades = new classes
- Testeable
- Mantenible
3. L — Liskov Substitution Principle (LSP)
Los objetos de una clase derivada pueden reemplazar objetos de la clase base sin afectar la correctitud.
// ❌ VIOLACIÓN: Rectangle vs Square
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int GetArea() => Width * Height;
}
public class Square : Rectangle
{
// Square viola LSP: Width y Height deben ser iguales
private int _size;
public override int Width
{
get => _size;
set => _size = value; // ❌ También cambia Height
}
public override int Height
{
get => _size;
set => _size = value;
}
}
// Bug:
var shape = new Square();
shape.Width = 5;
shape.Height = 3;
Console.WriteLine(shape.Width); // 3, no 5!
// ❌ Violaría expectativas de contrato
// ✅ CORRECTO: Jerarquía apropiada
public abstract class Shape
{
public abstract int GetArea();
}
public class Rectangle : Shape
{
public int Width { get; set; }
public int Height { get; set; }
public override int GetArea() => Width * Height;
}
public class Square : Shape
{
public int Side { get; set; }
public override int GetArea() => Side * Side;
}
// Ahora LSP se cumple
Shape shape = new Square { Side = 5 };
Console.WriteLine(shape.GetArea()); // 25, correcto
shape = new Rectangle { Width = 5, Height = 3 };
Console.WriteLine(shape.GetArea()); // 15, correcto
Beneficios:
- Comportamiento predecible de subclases
- Código que usa la clase base funciona con subclases
- Evita sorpresas y bugs
4. I — Interface Segregation Principle (ISP)
Los clientes no deberían depender de interfaces que no usan.
// ❌ VIOLACIÓN: Interfaz "gorda"
public interface IRepository
{
Task<T> GetByIdAsync<T>(int id);
Task SaveAsync<T>(T entity);
Task DeleteAsync<T>(int id);
Task<List<T>> GetAllAsync<T>();
Task<PagedResult<T>> GetPagedAsync<T>(int page, int pageSize);
Task BulkInsertAsync<T>(List<T> entities);
Task ExecuteSqlAsync(string sql);
Task<IEnumerable<T>> ExecuteQueryAsync<T>(string sql);
// ... 20 métodos más
}
public class OrderService
{
private readonly IRepository _repository; // ❌ Depende de 20 métodos, usa solo 3
public async Task<Order> GetOrderAsync(int id)
{
return await _repository.GetByIdAsync<Order>(id); // Solo este
}
}
// ✅ CORRECTO: Interfaces segregadas
public interface IReadRepository<T>
{
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
}
public interface IWriteRepository<T>
{
Task SaveAsync(T entity);
Task DeleteAsync(int id);
}
public interface IPagedRepository<T> : IReadRepository<T>
{
Task<PagedResult<T>> GetPagedAsync(int page, int pageSize);
}
public interface ISqlRepository
{
Task<IEnumerable<T>> ExecuteQueryAsync<T>(string sql);
}
// Ahora cada servicio depende SOLO de lo que usa
public class OrderService
{
private readonly IReadRepository<Order> _readRepo;
public async Task<Order> GetOrderAsync(int id)
{
return await _readRepo.GetByIdAsync(id);
}
}
public class OrderReportService
{
private readonly ISqlRepository _sqlRepo;
public async Task<Report> GenerateReportAsync()
{
var data = await _sqlRepo.ExecuteQueryAsync<ReportData>(
"SELECT ... FROM Orders WHERE ..."
);
return new Report(data);
}
}
Beneficios:
- Interfaces específicas y claras
- Cada cliente depende solo de lo que necesita
- Testing simplificado
- Bajo acoplamiento
5. D — Dependency Inversion Principle (DIP)
Depende de abstracciones, no de concreciones.
// ❌ VIOLACIÓN: Dependencia de implementaciones concretas
public class EmailService
{
private readonly SmtpClient _smtpClient; // ❌ Concretión
private readonly SqlConnection _dbConnection; // ❌ Concretión
public void SendOrderConfirmation(Order order)
{
var message = new MailMessage("...", order.CustomerEmail);
message.Body = "Order confirmed!";
_smtpClient.Send(message); // Acoplado a SMTP
var command = _dbConnection.CreateCommand();
command.CommandText = "INSERT INTO SentEmails ...";
_dbConnection.ExecuteNonQuery(); // Acoplado a SQL
}
}
// Problemas:
// - ❌ Imposible testear (requiere SMTP real)
// - ❌ Cambiar a SendGrid requiere modificar clase
// - ❌ Difícil reutilizar
// ✅ CORRECTO: Depender de abstracciones
public interface IEmailProvider
{
Task SendAsync(Email email);
}
public interface IAuditLog
{
Task LogEmailSentAsync(Guid orderId, string recipient);
}
public class EmailService
{
private readonly IEmailProvider _emailProvider; // ✅ Abstracción
private readonly IAuditLog _auditLog; // ✅ Abstracción
public EmailService(IEmailProvider emailProvider, IAuditLog auditLog)
{
_emailProvider = emailProvider;
_auditLog = auditLog;
}
public async Task SendOrderConfirmationAsync(Order order)
{
var email = new Email
{
From = "noreply@shop.com",
To = order.CustomerEmail,
Subject = "Order Confirmed",
Body = "..."
};
await _emailProvider.SendAsync(email); // Usa abstracción
await _auditLog.LogEmailSentAsync(order.Id, order.CustomerEmail);
}
}
// Implementaciones pueden cambiar sin afectar EmailService
public class SmtpEmailProvider : IEmailProvider
{
private readonly ISmtpClient _client;
public async Task SendAsync(Email email) => await _client.SendMailAsync(email);
}
public class SendGridEmailProvider : IEmailProvider
{
private readonly ISendGridClient _client;
public async Task SendAsync(Email email) => await _client.SendAsync(email);
}
// Para testing:
public class MockEmailProvider : IEmailProvider
{
public List<Email> SentEmails { get; } = new();
public Task SendAsync(Email email)
{
SentEmails.Add(email);
return Task.CompletedTask;
}
}
// Inyección de dependencias en Startup
services.AddScoped<IEmailProvider>(sp =>
Environment.GetEnvironmentVariable("EMAIL_PROVIDER") switch
{
"sendgrid" => new SendGridEmailProvider(...),
_ => new SmtpEmailProvider(...)
}
);
// En test:
var mockProvider = new MockEmailProvider();
var service = new EmailService(mockProvider, new MockAuditLog());
await service.SendOrderConfirmationAsync(order);
Assert.AreEqual(1, mockProvider.SentEmails.Count);
Beneficios:
- Altamente testeable
- Fácil cambiar implementaciones
- Bajo acoplamiento
- Flexible
Otros Principios Importantes
DRY — Don't Repeat Yourself
Evita código duplicado. Abstrae lógica común.
// ❌ REPETICIÓN
public class OrderValidator
{
public bool ValidateOrderEmail(Order order)
{
if (string.IsNullOrWhiteSpace(order.Email) ||
!order.Email.Contains("@"))
return false;
return true;
}
}
public class CustomerValidator
{
public bool ValidateCustomerEmail(Customer customer)
{
if (string.IsNullOrWhiteSpace(customer.Email) ||
!customer.Email.Contains("@"))
return false;
return true;
}
}
// ✅ ABSTRACCIÓN
public static class EmailValidator
{
public static bool IsValid(string email)
{
return !string.IsNullOrWhiteSpace(email) && email.Contains("@");
}
}
public class OrderValidator
{
public bool ValidateOrderEmail(Order order) => EmailValidator.IsValid(order.Email);
}
public class CustomerValidator
{
public bool ValidateCustomerEmail(Customer customer) => EmailValidator.IsValid(customer.Email);
}
KISS — Keep It Simple, Stupid
La solución más simple es usualmente la mejor. Evita over-engineering.
// ❌ OVER-ENGINEERING
public interface IStrategyFactory<T> where T : IBaseStrategy
{
T CreateStrategy(string type);
}
public abstract class StrategyRegistry
{
private readonly Dictionary<string, Func<IConfiguration, IStrategy>> _strategies;
}
// A veces una simple expresión switch es suficiente ✅
public static DiscountStrategy GetDiscountStrategy(Customer customer)
{
return customer.Type switch
{
"Premium" => new PremiumDiscountStrategy(),
"Gold" => new GoldDiscountStrategy(),
_ => new RegularDiscountStrategy()
};
}
YAGNI — You Aren't Gonna Need It
No implementes funcionalidades que "podrían ser útiles". Implementa lo que necesitas HOY.
// ❌ YAGNI VIOLATION: "Ah, podría necesitar soporte multi-moneda..."
public class Order
{
public decimal Total { get; set; }
public string Currency { get; set; }
public ExchangeRate ExchangeRate { get; set; }
public decimal ConvertedTotal { get; set; }
public decimal TaxByCountry { get; set; }
public int CountryId { get; set; }
// ... 10 más propiedades "por si acaso"
}
// ✅ Implementa solo lo que necesitas HOY
public class Order
{
public decimal Total { get; set; }
}
// Cuando necesites moneda, lo agregas:
public class Order
{
public Money Total { get; set; } // Value Object con Currency
}
Principios Arquitectónicos Avanzados
1. Separation of Concerns (SoC)
Cada parte del sistema debe manejar una preocupación única.
┌─────────────────────────┐
│ Presentation │ ← Cómo mostramos datos (MVC, API)
├─────────────────────────┤
│ Business Logic │ ← QUÉ hacer (Rules, Decisions)
├─────────────────────────┤
│ Data Access │ ← CÓMO guardar/obtener datos
├─────────────────────────┤
│ Cross-Cutting │ ← Logging, Auth, Caching
└─────────────────────────┘
2. DRY Principle ("No Repeat Yourself") Aplicado a Arquitectura
Evita duplicar lógica entre servicios/módulos.
3. High Cohesion, Low Coupling
HIGH COHESION LOW COUPLING
┌─────────────────┐ ┌─────────────────┐
│ Order Module │ │ Order Module │
├─────────────────┤ ├─────────────────┤
│ ・Order │ ▌│ ─ ─ ─ ─ ─ ─ ─│
│ ・LineItem │ │ References only │
│ ・Discount │ │ via ID │
│ ・Total Calc │ └─────────────────┘
│ logically │ │
│ related │ ├─ Weak dependency
│ ✅ GOOD │ ├─ easy to change
└─────────────────┘ ├─ easy to test
│
└─ Fuerte relación
✅ GOOD
4. Explicit Dependencies
Haz visibles todas las dependencias:
// ❌ IMPLÍCITAS: No se ve qué necesita
public class OrderService
{
public async Task<Order> CreateAsync(CreateOrderDto dto)
{
// ¿De dónde vienen las dependencias? No se sabe
var order = new Order(dto);
await SaveAsync(order);
return order;
}
}
// ✅ EXPLÍCITAS: Inyección por constructor
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly ILogger _logger;
public OrderService(
IOrderRepository repository, // ← Dependencias visibles
IEmailService emailService, // ← Claro qué necesita
ILogger logger) // ← Fácil testear
{
_repository = repository;
_emailService = emailService;
_logger = logger;
}
}
Checklist: ¿Cumple SOLID?
[ ] S - Single Responsibility
├─ ¿Esta clase tiene una única razón para cambiar?
└─ ¿Puedo describirla en menos de 10 palabras?
[ ] O - Open/Closed
├─ ¿Puedo extender sin modificar?
└─ ¿Uso polimorfismo cuando tengo variaciones?
[ ] L - Liskov Substitution
├─ ¿Subclase puede reemplazar clase base sin problemas?
└─ ¿Respeta el contrato de la clase base?
[ ] I - Interface Segregation
├─ ¿Mi interfaz es pequeña y específica?
└─ ¿Clientes usan todos los métodos?
[ ] D - Dependency Inversion
├─ ¿Dependo de abstracciones, no de concreciones?
└─ ¿Puedo testear fácilmente?
[ ] DRY - No Repeat Yourself
├─ ¿Hay código duplicado?
└─ ¿Podría abstraerlo en una función/clase?
[ ] KISS - Keep It Simple
├─ ¿Es mi solución simple de entender?
└─ ¿He over-engineered?
[ ] YAGNI - You Aren't Gonna Need It
├─ ¿Estoy implementando cosas "por si acaso"?
└─ ¿Es realmente necesario hoy?
Aplicando SOLID a un Sistema Real
// EJEMPLO: Sistema de pagos
// 1️⃣ S: Responsabilidades claras
public interface IPaymentProcessor { Task<PaymentResult> ProcessAsync(Payment p); }
public interface IPaymentValidator { bool Validate(Payment p); }
public interface IPaymentAuditLog { Task LogAsync(PaymentEvent e); }
public interface IFraudDetector { bool IsSuspicious(Payment p); }
// 2️⃣ O: Extensible para nuevas formas de pago
public interface IPaymentGateway { Task<GatewayResponse> SendAsync(Payment p); }
public class StripeGateway : IPaymentGateway { ... }
public class PayPalGateway : IPaymentGateway { ... }
// 3️⃣ L: Las subclases respetan el contrato
// Las clases payment pueden intercambiarse
// 4️⃣ I: Interfaces pequeñas
public interface INotificationService { Task NotifyAsync(Payment p); }
public interface IReconciliation { Task ReconcileAsync(Payment p); }
// 5️⃣ D: Inyección de dependencias
public class PaymentProcessingService
{
public PaymentProcessingService(
IPaymentValidator validator,
IFraudDetector fraudDetector,
IPaymentGateway gateway,
IPaymentAuditLog auditLog,
INotificationService notifier)
{ ... }
}
// Resultado: Extensible, testeable, mantenible ✅