🎯 SOLID — 5 princípios contra código frágil
📚 Os 5 Princípios SOLID
Criados por Robert C. Martin (Uncle Bob), os princípios SOLID são a base para escrever código orientável a objetos que não se torna uma bola de lama com o tempo. Exemplo clássico de violação: um PaymentProcessor que valida cartão, calcula taxa, processa pagamento, envia email e gera relatório — viola praticamente todos os princípios.
Uma classe deve ter apenas uma razão para mudar. Se você precisa alterar uma classe por motivos diferentes (mudança de regra de negócio E mudança de formato de relatório), ela faz coisa demais.
Aberto para extensão, fechado para modificação. Adicione novos comportamentos sem alterar código existente — use interfaces, herança ou composição.
Subtipos devem ser substituíveis por seus tipos base sem quebrar o programa. Se Square herda de Rectangle mas muda o comportamento de setWidth, viola LSP.
Nenhum cliente deve ser forçado a depender de métodos que não usa. Prefira várias interfaces específicas a uma interface única e gorda.
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações (interfaces).
❌ Evitar — Violações Comuns
- ●
UserServiceque faz CRUD, envia email, gera PDF e valida permissões (viola SRP) - ●Adicionar
if/elsepara cada novo tipo de pagamento em vez de criar novo handler (viola OCP) - ●Interface
IWorkercomwork()eeat()— robôs não comem (viola ISP) - ●Instanciar
new MySQLConnection()dentro da lógica de negócio (viola DIP)
✅ Fazer — Código Correto
- ●Classes pequenas e focadas:
UserRepository,EmailSender,PdfGenerator - ●Strategy pattern para novos tipos:
PaymentStrategyinterface + implementações - ●Interfaces segregadas:
IWorkable,IFeedableseparados - ●Injetar
IDatabasevia construtor em vez de instanciar concreto
💡 Dica Prática
Não tente aplicar SOLID de forma dogmática desde o início. Comece escrevendo código simples e refatore quando sentir dor: quando uma classe cresce demais, quando um switch/case aparece pela terceira vez, quando um teste fica impossível de escrever. SOLID resolve problemas reais — não é checklist para código novo.
💡 DRY, KISS e YAGNI — Simplicidade como virtude
📚 Os 3 Princípios da Simplicidade
Enquanto SOLID foca em orientação a objetos, estes três princípios são universais — aplicam-se a qualquer paradigma, linguagem ou escala. O mantra: faça simples, não repita, não antecipe.
Cada pedaço de conhecimento deve ter uma única representação autoritativa no sistema. Não confunda com “nunca copie código” — é sobre conhecimento, não sobre linhas de código.
A maioria dos sistemas funciona melhor quando são mantidos simples. Complexidade deve ser adicionada por necessidade, não por prevenção. Se uma solução precisa de um diagrama de 30 min para ser explicada, provavelmente há uma mais simples.
Não implemente funcionalidade até que ela seja realmente necessária. Exemplo: criar um ValidationFramework inteiro quando 3 linhas de if resolveriam para um time de 2 pessoas.
⚠️ Alerta: DRY mal aplicado gera abstrações prematuras
O erro mais comum é ver 2 trechos de código parecidos e imediatamente extrair uma função “genérica”. Resultado: uma abstração que não serve bem para nenhum dos dois casos e acopla código que deveria evoluir independentemente. A regra de ouro: espere a terceira repetição (Rule of Three) antes de abstrair. Código duplicado é melhor que a abstração errada.
📊 Dados que Importam
das features “para o futuro” nunca são usadas (IBM Research)
das features de software são raramente ou nunca utilizadas (Standish Group)
mais caro manter código complexo vs. reescrever simples (IEEE Study)
🧬 Design Patterns Essenciais
📚 Categorias de Patterns (GoF)
Os Design Patterns do Gang of Four (1994) são vocabulário compartilhado. Quando você diz “usa Strategy aqui”, o time inteiro entende. Não é sobre decorar 23 patterns — é sobre reconhecer problemas recorrentes.
🏗️ Creational
Como objetos são criados
- • Factory Method — criação via subclasses
- • Abstract Factory — famílias de objetos
- • Builder — construção passo a passo
- • Singleton — instância única (cuidado!)
🔧 Structural
Como objetos são compostos
- • Adapter — interface compatível
- • Facade — interface simplificada
- • Decorator — comportamento extra
- • Proxy — acesso controlado
🚀 Behavioral
Como objetos se comunicam
- • Strategy — algoritmos intercambiáveis
- • Observer — notificação de mudanças
- • Command — ações como objetos
- • Template Method — esqueleto de algoritmo
🕑 Exemplos Práticos no Mundo Real
Processadores de pagamento como strategies. Adicionar Pix? Crie PixPaymentStrategy sem tocar no core do checkout.
Estado muda → UI re-renderiza automaticamente. O componente “observa” o state e reage sem acoplamento direto.
Spring, NestJS, .NET — o container cria objetos para você. Você pede ILogger, o factory decide se é Console, File ou CloudWatch.
Migrando de Twilio para AWS SNS? Crie um adapter que implementa a interface antiga usando o SDK novo. Zero mudança no código de negócio.
💡 Quando Usar Patterns
Patterns são soluções para problemas recorrentes. Se você não tem o problema, não use o pattern. Não crie um Abstract Factory para criar 1 tipo de objeto. Não use Observer para 2 componentes. A regra: sinta a dor primeiro, depois aplique o remédio. O excesso de patterns é tão prejudicial quanto a falta deles — chama-se “pattern-itis”.
🔌 Acoplamento e Coesão
📚 Tipos de Acoplamento (do pior ao melhor)
Acoplamento é o grau de dependência entre módulos. Coesão é o grau em que elementos de um módulo pertencem juntos. O objetivo é sempre: baixo acoplamento, alta coesão. Exemplo clássico: se seu módulo de auth depende diretamente do módulo de billing, uma mudança no billing pode derrubar o login.
Um módulo acessa diretamente dados internos de outro. O pior tipo.
Módulos compartilham variáveis globais. Mudar uma afeta todos.
Um módulo controla o fluxo de outro via flags ou parâmetros.
Módulos compartilham estruturas de dados compostas, usando apenas parte delas.
Módulos se comunicam apenas via parâmetros simples. O melhor tipo.
❌ Evitar
- ●Serviços que acessam o banco de dados de outros serviços diretamente
- ●Variáveis globais compartilhadas entre módulos
- ●Dependências circulares: A depende de B que depende de A
- ●Classes “God Object” que sabem e fazem tudo
✅ Fazer
- ●Comunicar via APIs/contratos bem definidos entre serviços
- ●Usar eventos para desacoplar (event-driven architecture)
- ●Agrupar código que muda junto (alta coesão funcional)
- ●Dependency injection para inverter dependências
📊 Métricas de Acoplamento
Efferent Coupling
Quantos módulos este módulo depende. Alto Ce = módulo frágil.
Afferent Coupling
Quantos módulos dependem deste. Alto Ca = mudanças são arriscadas.
Instabilidade
I = Ce/(Ca+Ce). Varia de 0 (estável) a 1 (instável). Módulos estáveis devem ser abstratos.
📦 Separação de Concerns
📚 Vertical vs. Horizontal Slicing
Cada parte do sistema deve lidar com uma única preocupação. Quando SQL aparece no template HTML (violação clássica), mudar o layout pode quebrar a query — e vice-versa.
↔️ Horizontal Slicing (por camada)
Separação técnica: Controllers, Services, Repositories, Models. Tradicional e familiar.
controllers/
UserController
OrderController
services/
UserService
OrderService
repositories/
UserRepository
OrderRepository
↕️ Vertical Slicing (por feature)
Separação por domínio: tudo de User junto, tudo de Order junto. Escala melhor em times grandes.
users/
UserController
UserService
UserRepository
orders/
OrderController
OrderService
OrderRepository
❌ Evitar
- ●SQL queries dentro de templates HTML/JSX
- ●Regras de negócio no controller da API
- ●Lógica de UI no backend (formatação de moeda, cores)
- ●Logging, auth e validação espalhados em todo lugar (cross-cutting mal resolvido)
✅ Fazer
- ●Camada de acesso a dados separada da lógica de negócio
- ●Middlewares para cross-cutting concerns (auth, logging, rate limit)
- ●DTOs para isolar modelo interno do modelo de API
- ●Organização por feature quando o time cresce acima de 5 devs
💡 Dica Prática
O teste da separação: consigo trocar o banco de dados sem mudar regras de negócio? Consigo trocar o framework web sem reescrever domínio? Se a resposta é “não”, seus concerns estão misturados. Comece separando o que muda por razões diferentes — UI muda por UX, negócio muda por regras, infra muda por performance.
🛡️ SRP na Arquitetura
📚 Bounded Context — SRP em escala
Na arquitetura, SRP significa: um serviço, uma razão para mudar. Isso se conecta diretamente ao conceito de Bounded Context do Domain-Driven Design (DDD). Cada contexto tem seu próprio modelo, vocabulário e limites claros.
Exemplo: Um UserService que gerencia autenticação, perfil, preferências e billing é um monolito disfarçado de microsserviço. Decomposição correta:
Auth Service
Login, registro, tokens, MFA
Profile Service
Nome, avatar, bio, settings
Preferences
Notificações, tema, idioma
Billing Service
Planos, cobrança, faturas
⚠️ Alerta: Mini-Monolitos Distribuídos
O erro mais comum em microsserviços: decompor por entidade técnica em vez de por domínio de negócio. Resultado: 20 serviços que precisam ser deployados juntos, que compartilham banco de dados e que falham em cascata. Você não tem microsserviços — você tem um monolito distribuído, que é o pior dos dois mundos: complexidade de distribuído sem os benefícios da independência.
❌ Evitar
- ●Serviços que precisam ser deployados em ordem específica
- ●Serviços que compartilham o mesmo banco de dados
- ●Um “serviço” com 15+ endpoints que faz de tudo
- ●Decomposição por camada técnica (DatabaseService, CacheService)
✅ Fazer
- ●Decompor por domínio de negócio (Pagamentos, Entregas, Catálogo)
- ●Cada serviço é dono do seu banco de dados
- ●Deploy independente: mudar billing não exige redeploy de auth
- ●Comunicação via contratos (APIs, eventos) não via banco compartilhado
🔄 Inversão de Dependência
📚 Dependency Injection na Prática
O Dependency Inversion Principle (DIP) diz: dependa de abstrações, não de detalhes. Dependency Injection (DI) é a técnica para aplicar o DIP — em vez de criar dependências internamente, você as recebe de fora.
❌ Sem DIP — Acoplamento direto
private db = new PostgresDB();
private mailer = new SendGrid();
// Lógica presa ao Postgres e SendGrid
// Impossível testar sem infra real
}
✅ Com DIP — Dependência invertida
constructor(
private db: IDatabase,
private mailer: IMailer
) {}
// Funciona com qualquer implementação
}
🕑 Refactoring para DIP — Passo a Passo
Procure por new ConcreteClass() dentro de lógica de negócio. Cada new é um ponto de acoplamento.
Crie IDatabase, IMailer, IPaymentGateway — contratos que definem o que você precisa, não como é feito.
Mude a classe para receber dependências como parâmetros do construtor em vez de criá-las internamente.
Na composição root (main/bootstrap), registre as implementações concretas. Apenas este ponto conhece os detalhes.
💡 Testabilidade como Benefício
O maior benefício prático da inversão de dependência é a testabilidade. Sua lógica de “aprovar pedido” não deve importar se usa PostgreSQL ou MongoDB — injete a interface, não a implementação. Nos testes, injete um InMemoryDatabase e um FakeMailer. Testes rodam em milissegundos, sem Docker, sem rede, sem side effects. Se testar é difícil, é sinal de que suas dependências estão invertidas errado.
📋 Checklist do Módulo 1.3
Antes de avançar para o próximo módulo, verifique se você consegue responder:
- ☐ Consigo explicar cada letra do SOLID com um exemplo prático
- ☐ Sei a diferença entre DRY (conhecimento) e eliminação ingênua de duplicação
- ☐ Reconheço quando YAGNI se aplica vs. quando antecipar faz sentido
- ☐ Identifico Factory, Strategy e Observer em frameworks que já uso
- ☐ Sei medir acoplamento (Ce, Ca) e entendo os tipos de acoplamento
- ☐ Consigo explicar vertical vs. horizontal slicing e quando usar cada um
- ☐ Reconheço mini-monolitos distribuídos e sei como evitá-los
- ☐ Consigo refatorar código para usar DIP e Dependency Injection
Próximo Módulo
No Módulo 1.4 — 🔒 Qualidade e Atributos de Qualidade, vamos explorar os “-ilities” que definem o sucesso de um sistema: performance, segurança, disponibilidade, escalabilidade, manutenibilidade, observabilidade e testabilidade.