Introdução: quando o SOLID deixa de ser teoria
A maioria dos desenvolvedores conhece a sigla SOLID no nível "a classe deve ser responsável por uma coisa". No entanto, em projetos reais em C# (.NET) com Dependency Injection, microsserviços e padrões assíncronos, seguir os princípios ingenuamente leva ao efeito oposto — complexidade excessiva, inchaço de interfaces e violação de encapsulamento.
Neste artigo, vamos abordar o SOLID não como um conjunto de dogmas, mas como uma ferramenta para tomar decisões arquiteturais. Você aprenderá como aplicar corretamente os princípios em cenários avançados: trabalho com contêineres IoC, composição em vez de herança, tratamento de erros através de interfaces e teste de sagas.
1. Single Responsibility: limites de responsabilidade em DDD
O princípio da responsabilidade única (SRP) é frequentemente confundido com "uma classe — uma função". Na verdade, trata-se da razão para mudar (reason to change). No contexto de C# e Domain-Driven Design, isso significa que uma classe deve mudar apenas por um motivo, relacionado a um único ator ou regra de negócio.
Antipadrão avançado: "Entity Divina"
Considere um exemplo típico de violação do SRP em .NET — a classe OrderEntity, que simultaneamente gerencia o estado do pedido, calcula impostos, envia e-mail e salva dados no banco de dados via Entity Framework.
// ❌ Violação do SRPpublic class OrderEntity{ public int Id { get; set; } public decimal Total { get; set; } public string Email { get; set; }
public void CalculateTax() { /* lógica de impostos */ } public void SendConfirmation() { /* envio de e-mail */ } public void SaveToDatabase() { /* EF Core SaveChanges */ }}Problema: uma alteração na regra de negócio sobre impostos (por exemplo, uma nova lei) levará a uma modificação na classe que também é responsável pelo envio de notificações. Isso aumenta o risco de regressão.
Solução: separação de serviços via DI
// ✅ SRP através de serviçospublic class Order{ public int Id { get; set; } public decimal Total { get; set; } // Apenas estado, nenhuma lógica}
public class TaxCalculator : ITaxCalculator{ public decimal Calculate(Order order) { /* ... */ }}
public class EmailService : INotificationService{ public Task SendConfirmation(Order order) { /* ... */ }}
public class OrderRepository : IRepository<Order>{ public Task SaveAsync(Order order) { /* EF Core */ }}Agora cada classe tem exatamente uma razão para mudar. O contêiner DI (Microsoft.Extensions.DependencyInjection) as conecta através dos construtores.
2. Open/Closed: extensão através de estratégia e decorador
O princípio aberto/fechado (OCP) afirma: uma classe deve estar aberta para extensão, mas fechada para modificação. Em .NET, isso é alcançado não apenas através de herança, mas também através de composição com os padrões Strategy e Decorator.
Caso avançado: validação com crescimento de regras
Imagine que você tem um sistema de validação de pedidos. A versão inicial verificava apenas o valor e o endereço. Um mês depois, foi adicionada a regra "não é possível pedir itens com valor acima do limite". Em vez de modificar a classe OrderValidator, usamos composição.
// ❌ Violação do OCP — modificação de classe existentepublic class OrderValidator{ public bool Validate(Order order) { if (order.Total < 0) return false; if (string.IsNullOrEmpty(order.Address)) return false; // Nova regra — modificamos a classe if (order.Total > 10000) return false; return true; }}// ✅ OCP através de composição de validadorespublic interface IValidator<T>{ bool Validate(T entity);}
public class TotalValidator : IValidator<Order>{ public bool Validate(Order order) => order.Total > 0;}
pu