Revisão de Design SOLID
Direciona o agente a percorrer uma classe ou módulo C# através de cada princípio SOLID usando lentes concretas do .NET, nomeando o cheiro de código, o princípio violado e a refatoração mínima — priorizando violações que causam acoplamento real ou retrabalho, não apenas falhas teóricas.
Quando usar
- Uma classe está se tornando difícil de gerenciar — abrange E/S, lógica de negócios, mapeamento e efeitos colaterais em um único arquivo.
- Um PR introduz uma nova variante de tipo e uma cadeia
switchouif-elseexistente deve ser editada para acomodá-la. - Construtores instanciam suas próprias dependências com
newou referenciamDateTime.Now/ ajudantesstaticdiretamente. - Uma interface acumulou membros para satisfazer vários chamadores não relacionados, forçando implementações vazias.
Processo
- Identifique a classe/módulo em revisão. Nomeie sua responsabilidade declarada. Se o nome exigir "e" ou "ou" para descrevê-la, isso já é um sinal.
- Percorra cada princípio SOLID em ordem usando as lentes C# abaixo. Trate cada um como uma passagem separada; não pare após a primeira descoberta.
- Nomeie o cheiro de código e o princípio violado. "Construtor pesado montando seu próprio
HttpClienteSqlConnection" → DIP; "substituição de base lançandoNotSupportedException" → LSP. Seja preciso. - Proponha a refatoração mínima com o mecanismo C#. Nomeie o tipo, padrão ou alteração de registro de DI — não apenas "aplique o princípio." Conselhos genéricos como "siga SOLID" não são uma descoberta.
- Priorize pelo impacto real. Um serviço gigante que mistura persistência EF com envio de e-mail causa retrabalho a cada sprint; um
IOrderServicecom 12 membros que nenhum chamador usa completamente é uma violação genuína de ISP. Uma classesealedque atende a dois casos de uso fracamente relacionados não quebra SRP se tiver um único motivo para mudar. Distinga violações que prejudicam daquelas que são meramente impuras.
Verificações .NET / Azure
SRP — Princípio da Responsabilidade Única
- Serviços gigantes / classes gigantes. Uma única classe que toca EF Core (
_context.SaveChangesAsync), envia e-mail (_emailSender.SendAsync), valida regras de negócio e mapeia para DTOs tem múltiplas razões para mudar. Extraia umIOrderValidator, umIOrderRepository(ou use o DbContext diretamente em uma classe de persistência dedicada) e umIOrderNotifiercomo colaboradores separados. - Controladores pesados. Um controlador ASP.NET Core que contém lógica
if/validação, chama o banco de dados diretamente e formata payloads de resposta está fazendo três tarefas. Controladores devem orquestrar: chamar um serviço, mapear o resultado e retornarIActionResult. Regras de negócio pertencem à camada de aplicação/domínio. - "E" no nome do tipo ou responsabilidade.
OrderValidationAndPersistenceServiceé um cheiro de nomenclatura que sinaliza violação de SRP. Se você só consegue descrever a classe usando "e", divida-a. - Múltiplas razões para mudar. Pergunte: "Esta classe precisaria mudar se trocássemos de provedor de e-mail? Se alterássemos o esquema do banco de dados? Se as regras de negócio mudassem?" Mais de um "sim" significa mais de uma responsabilidade.
OCP — Princípio Aberto/Fechado
switch/if-elsecrescente em um discriminador de tipo. Um blocoswitch(order.Type)dentro de um serviço que deve ser reaberto para cada novo valor de enumOrderTypeviola OCP. Substitua por uma estratégia: definaIOrderHandler(ouIOrderProcessor) e registre uma implementação por tipo; resolva viaIEnumerable<IOrderHandler>injetado no coordenador, despachando emhandler.Handles(order.Type).- Expansão de comportamento codificada. Se adicionar uma nova regra de desconto, canal de notificação ou formato de exportação exige editar uma classe existente, em vez de adicionar um novo
IDiscountRuleouIExporter, a classe não está fechada para modificação. Introduza a abstração antes do segundo caso chegar. - Anti-padrão de discriminador de tipo.
if (order is PremiumOrder)ouif (typeof(T) == typeof(FooOrder))dentro de uma classe de processamento é um cheiro de código equivalente aoswitchacima. Polimorfismo ou um registro de estratégia eliminam isso.
LSP — Princípio da Substituição de Liskov
- Substituições que lançam
throw new NotSupportedException()/NotImplementedException(). Um subtipo que não pode honrar um contrato base (ex.:ReadOnlyCollection<T>lançando emAdd) viola LSP. Se a substituição não pode ser implementada de forma significativa, a hierarquia está errada — prefira composição ou interfaces separadas. - Subtipos enfraquecendo pós-condições. Se a classe base garante que
GetOrders()nunca retornanull, uma substituição que retornanullviola o contrato que os chamadores usam. Documente invariantes (comentários XML ou contratos) e os aplique nas substituições. - Subtipos fortalecendo pré-condições. Uma substituição que adiciona uma verificação de nulo não presente na base, ou que rejeita uma faixa de parâmetros válida que a base aceitava, quebra o código que passa entrada válida do contrato base. O subtipo deve aceitar tudo que a base aceitava.
- Downcasts generalizados com
is/asno código chamador.if (handler is SpecialHandler sh) sh.SpecialMethod()no chamador significa que a abstração está quebrada — ou o método pertence à interface base, ou os tipos não deveriam compartilhar uma hierarquia. Downcasts frequentes são um sinal de falha de LSP / abstração.
ISP — Princípio da Segregação de Interfaces
- Interfaces pesadas (10+ membros) com preocupações não relacionadas. Um
IOrderServiceexpondoCreateOrder,CancelOrder,GetInvoice,SendConfirmation,ExportToCsv,CalculateTax,ValidateAddresseGetShippingRatesforça cada implementador e duplo de teste a implementar métodos que não usa. Divida em interfaces de função:IOrderWriter,IOrderReader,IOrderNotifier,IOrderExporter. - Implementações vazias / stubs com
NotImplementedException. Se uma implementação mock ou de teste retornathrow new NotImplementedException()para metade dos membros da interface, esses membros pertencem a uma interface de função diferente. A presença de operações vazias é evidência direta de violação de ISP. - Clientes importando métodos que nunca chamam. Se
PaymentServicedepende deIOrderServicemas só chamaGetOrder, deveria depender deIOrderReader. Estreitar a dependência torna o acoplamento explícito e o teste mais barato.
DIP — Princípio da Inversão de Dependência
- Instanciar dependências concretas com
newdentro de uma classe.new HttpClient()construído dentro de um serviço cria uma dependência oculta, ignora o contêiner de DI, vaza soquetes (sem pooling de conexão) e torna a classe não testável sem truques de reflexão. InjeteIHttpClientFactory(cliente tipado) ou um cliente nomeado via construtor. Mesmo padrão paranew SqlConnection(connectionString)— injeteIDbConnectionFactoryou deixe EF Core / Dapper gerenciarem a conexão viaDbContextregistrado. - Depender de concretos em vez de abstrações. Um construtor tipado para
SqlOrderRepositoryem vez deIOrderRepositoryacopla a classe à tecnologia de armazenamento. Qualquer classe que deva ser reutilizável ou testável isoladamente deve depender apenas de interfaces ou tipos abstratos. - Acoplamento oculto a
DateTime.Now/ estado estático.var now = DateTime.Nowdentro de um método de negócio torna a classe não determinística e não testável sem truques de viagem no tempo. IntroduzaISystemClock(ou useTimeProviderdo .NET 8+) e o injete. O mesmo se aplica aEnvironment.MachineName,Random.Sharedou qualquer outro estático ambiente. - Singletons mutáveis estáticos. Um
static Dictionary<string, Order> _cacheem uma classe de serviço é estado compartilhado oculto que ignora o gerenciamento de tempo de vida da DI e não é seguro para uso concorrente. Substitua porIMemoryCache/IDistributedCacheinjetados via DI, que suportam escopo adequado e expurgo.
Nota de julgamento: nem todo
newé uma violação de DIP.new OrderDto(order)(um simples recipiente de dados) ounew List<T>()(uma coleção padrão) são aceitáveis. DIP importa quando o objeto construído tem comportamento, E/S externa ou configuração — ou seja, quando trocá-lo por um duplo de teste ou implementação alternativa seria valioso.
Sinais vermelhos
| Sinal | Por que é importante |
|---|---|
new HttpClient() construído dentro de um método ou construtor de serviço | Ignora o pooling de conexão de IHttpClientFactory (exaustão de soquetes sob carga), esconde a dependência e torna a classe não testável sem E/S de produção. |
switch(order.Type) ou if (x is ConcreteType) em uma classe coordenadora | Deve ser reaberto para cada nova variante; chamadores sabem demais sobre a hierarquia de tipos. Substitua por IEnumerable<IHandler> resolvido da DI. |
throw new NotImplementedException() ou throw new NotSupportedException() em uma substituição não abstrata | O subtipo não pode honrar o contrato base — violação de LSP. Implemente o método corretamente ou reestruture a hierarquia. |
| Uma interface com 10+ membros onde os chamadores usam 2–3 deles | Força grandes duplos de teste; sinaliza que múltiplas interfaces de função foram colapsadas em uma. |
Uma classe construindo seu próprio DbContext via new AppDbContext(options) | Ignora o gerenciamento de tempo de vida da DI (Scoped vs Transient vs Singleton), impede o pooling de conexões e quebra a testabilidade. Injete AppDbContext ou IDbContextFactory<AppDbContext>. |
DateTime.Now chamado diretamente dentro de um método de negócio | Não determinístico em testes; acoplamento ao relógio do sistema esconde uma dependência lógica. Injete TimeProvider (.NET 8+) ou ISystemClock. |
Uma ação de controlador contendo lógica de negócio if/switch ou consultas EF | O controlador assumiu responsabilidades que pertencem à camada de aplicação/domínio; uma única mudança nas regras de negócio exige tocar no controlador. |
Campo mutável static usado como cache em processo | Não seguro para threads sem bloqueio explícito; ignora escopo da DI; não pode ser expurgado ou observado. Use IMemoryCache. |
Exemplo
Veja examples/solid-review/.