📐 Capítulo 05: SOLID I: Princípios SRP, OCP e LSP
Módulo 10: Paradigmas de Programação & Padrões de Projeto • Bloco 2: SOLID e GRASP
🏛️ A Tríade Fundamental do Design Orientado a Objetos: Os cinco princípios SOLID, consolidados por Robert C. Martin (“Uncle Bob”) no início dos anos 2000, representam as fundações da arquitetura de software sustentável. Neste capítulo, exploramos a fundo os três primeiros pilares: a Responsabilidade Única (SRP), a Extensibilidade Fechada (OCP) e o Contrato de Substituição Formal de Liskov (LSP).
🗺️ Mapa Conceitual do Capítulo
flowchart TD
SOLID["SOLID (Parte I: Fundamentos Estruturais)"]
SOLID --> SRP["1. Single Responsibility Principle (SRP)<br/>• Um módulo deve ter apenas uma razão para mudar<br/>• Separação por Atores de Negócio (CEO, CFO, CTO)<br/>• Eliminação do Anti-Padrão God Class"]
SOLID --> OCP["2. Open / Closed Principle (OCP)<br/>• Aberto para Extensão, Fechado para Modificação<br/>• Polimorfismo e Inversão de Controle<br/>• Extensões via Plugins sem tocar no core"]
SOLID --> LSP["3. Liskov Substitution Principle (LSP)<br/>• Subtipos devem honrar os contratos dos supertipos<br/>• Pré-condições não fortalecidas / Pós não enfraquecidas<br/>• Invariantes preservadas & Eliminação de instanceof"]
style SOLID fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style SRP fill:#e8f5e9,stroke:#4caf50,stroke-width:1.5px
style OCP fill:#fff3e0,stroke:#ff9800,stroke-width:1.5px
style LSP fill:#fce4ec,stroke:#e91e63,stroke-width:1.5px
🏛️ 1. Gênese Teórica e a Filosofia do SOLID
No final da década de 1990 e início dos anos 2000, Robert C. Martin reuniu e sistematizou princípios fundamentais formulados por pioneiros da ciência da computação (como Bertrand Meyer e Barbara Liskov) sob o acrônimo mnemônico SOLID:
- S – Single Responsibility Principle (SRP)
- O – Open/Closed Principle (OCP)
- L – Liskov Substitution Principle (LSP)
- I – Interface Segregation Principle (ISP)
- D – Dependency Inversion Principle (DIP)
O objetivo primário desses princípios é conter o que Martin define como as quatro patologias degenerativas do design de software:
- Rigidez (Rigidity): A tendência do software de ser difícil de alterar, pois qualquer mudança gera uma cascata de alterações em módulos dependentes.
- Fragilidade (Fragility): A tendência do software de quebrar em lugares não relacionados sempre que uma alteração é introduzida.
- Imobilidade (Immobility): A incapacidade de reutilizar componentes em outros sistemas porque eles estão fortemente amarrados ao contexto original.
- Viscosidade (Viscosity): Quando fazer a coisa certa pelo design é mais difícil do que recorrer a atalhos e “gambiarras” (hacks).
🎯 2. Princípio da Responsabilidade Única (SRP)
“A class should have one, and only one, reason to change.” — Robert C. Martin
2.1 A Reinterpretação Moderna: Um Módulo para um Ator
A frase clássica costuma ser mal interpretada por iniciantes, que tentam decompor classes até que elas tenham apenas um único método. Martin esclareceu a definição formal:
- Um módulo ou classe deve ser responsável perante um, e apenas um, ator de negócio.
- Um ator é um grupo de usuários, stakeholders ou departamentos da empresa que demandam alterações no sistema (por exemplo: Contabilidade, Recursos Humanos, Operações, Marketing).
Anti-Padrão: Classe Centralizada que atende múltiplos Atores
┌──────────────────────────────────────────────┐
│ Funcionario │
├──────────────────────────────────────────────┤
│ + calcularSalarioLiquido() [Ator: CFO / RH]│ ◄── Alteração no cálculo
│ + emitirRelatorioHoras() [Ator: COO / Op]│ pode quebrar o relatório
│ + salvarNoBancoDeDados() [Ator: CTO / TI]│ ou a persistência!
└──────────────────────────────────────────────┘
Se a equipe contábil solicita uma alteração na fórmula de cálculo de horas extras e o desenvolvedor modifica a classe Funcionario, ele corre o risco inaceitável de introduzir regressões no relatório operacional de turnos ou no layout da folha de pagamento.
2.2 Sinais de Violação e Métricas de Coesão
- Anti-Padrão God Class (Blob): Classes que acumulam milhares de linhas de código, dezenas de imports não relacionados e realizam validação, persistência SQL, geração de PDF e chamadas de API externa.
- Métrica LCOM (Lack of Cohesion of Methods): Mede o compartilhamento de atributos entre métodos de uma classe. Se os métodos de uma classe operam sobre subconjuntos completamente disjuntos de campos, a classe possui alta falta de coesão e deve ser dividida em classes separadas.
- Conflitos Frequentes de Git Merge: Quando desenvolvedores de times diferentes alteram o mesmo arquivo
.javaou.pyrotineiramente na mesma sprint, a classe é um ímã de responsabilidades cruzadas.
🔓 3. Princípio Aberto / Fechado (OCP)
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” — Bertrand Meyer (1988)
3.1 Meyer vs. Martin: Da Herança ao Polimorfismo
- OCP de Bertrand Meyer (1988): Focado em Eiffel e herança clássica. Uma vez que um módulo é compilado e homologado, seu código-fonte é considerado fechado; extensões ocorrem criando subclasses que herdam do módulo base.
- OCP Polimórfico de Martin (Anos 2000): Focado em interfaces e inversão de controle. O módulo fecha-se contra alterações através de abstrações (interfaces/contratos puros), permitindo que novos comportamentos sejam injetados via novas implementações de plugins sem que nenhuma linha do módulo consumidor precise ser reescrita ou recompilada.
flowchart TD
CLIENT["ProcessadorDePagamento<br/>(FECHADO para modificação)"]
CLIENT --> INTF["«interface»<br/>MetodoPagamento<br/>+ processar(valor)"]
INTF -.-> P1["PixPagamento<br/>(Extensão Nova)"]
INTF -.-> P2["CartaoCredito<br/>(Extensão Nova)"]
INTF -.-> P3["BoletoBancario<br/>(Extensão Nova)"]
INTF -.-> P4["CriptoMoeda<br/>(Extensão Nova)"]
style CLIENT fill:#ffebee,stroke:#f44336,stroke-width:2px
style INTF fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style P1 fill:#e8f5e9,stroke:#4caf50,stroke-width:1px
style P2 fill:#e8f5e9,stroke:#4caf50,stroke-width:1px
style P3 fill:#e8f5e9,stroke:#4caf50,stroke-width:1px
style P4 fill:#e8f5e9,stroke:#4caf50,stroke-width:1px
3.2 O Perigo dos Laços switch / if-else Infinitos
O sintoma clássico de violação do OCP é a presença recorrente de blocos condicionais inspecionando enums ou tipos literais:
# VIOLAÇÃO DO OCP: Toda nova operadora exige alterar este método já testado!
def calcular_frete(tipo_transportadora: str, peso: float) -> float:
if tipo_transportadora == "CORREIOS_PAC":
return peso * 12.5 + 10.0
elif tipo_transportadora == "CORREIOS_SEDEX":
return peso * 22.0 + 25.0
elif tipo_transportadora == "LOGGI_EXPRESS":
return peso * 18.0 + 15.0
# Adicionar FEDEX aqui exige alterar código em produção!
Ao refatorar para o OCP com uma interface comum CalculadoraFrete, novas operadoras tornam-se classes autônomas plugadas em tempo de execução, garantindo risco zero de quebra para os algoritmos já validados.
🔄 4. Princípio de Substituição de Liskov (LSP)
“Let $\phi(x)$ be a property provable about objects $x$ of type $T$. Then $\phi(y)$ should be true for objects $y$ of type $S$ where $S$ is a subtype of $T$.” — Barbara Liskov (1987)
O Princípio de Substituição de Liskov estabelece as condições formais de corretude semântica para o subtipo. Uma subclasse não pode apenas ter a mesma assinatura sintática da superclasse; ela deve cumprir rigorosamente o seu contrato comportamental.
4.1 As Regras Formais do Design by Contract (DbC)
Para que $S$ seja um substituto lícito de $T$:
- Pré-condições Não Podem Ser Fortalecidas: Uma subclasse não pode exigir mais do que a classe base exigia (ex.: se a classe base aceita qualquer número inteiro, a subclasse não pode exigir apenas inteiros positivos).
- Pós-condições Não Podem Ser Enfraquecidas: A subclasse não pode garantir menos do que a superclasse prometia retornar (ex.: se a superclasse garantia devolver uma lista ordenada não nula, a subclasse não pode devolver
nullou dados desordenados). - Invariantes Devem Ser Preservadas: Todas as regras estáticas de integridade mantidas pela superclasse devem permanecer intactas na subclasse.
- Restrição de Histórico (History Constraint): Uma subclasse não pode permitir mutações de estado que a superclasse definia como imutáveis.
4.2 O Dilema Clássico do Quadrado e Retângulo
Na matemática euclidiana, “todo quadrado é um retângulo”. No entanto, na modelagem orientada a objetos com estado mutável, essa relação de herança viola catastroficamente o LSP:
class Retangulo:
def __init__(self, largura: float, altura: float):
self._largura = largura
self._altura = altura
def set_largura(self, l: float): self._largura = l
def set_altura(self, a: float): self._altura = a
def calcular_area(self) -> float: return self._largura * self._altura
class Quadrado(Retangulo):
# Violação do LSP: ao alterar um lado, altera o outro forçadamente
def set_largura(self, l: float):
self._largura = l
self._altura = l
def set_altura(self, a: float):
self._largura = a
self._altura = a
Se uma função cliente espera um Retangulo e testa a invariante:
def redimensionar(r: Retangulo):
r.set_largura(5)
r.set_altura(4)
assert r.calcular_area() == 20, "Erro: área esperada é 20!"
Ao passar um objeto Quadrado, o assert explode com falha (calcular_area() resultará em 16), provando que o subtipo quebrou o comportamento esperado da superclasse.
4.3 O Anti-Padrão da Ave que Não Voa e Exceções Abstratas
Outra manifestação grave é criar subclasses que herdam métodos aos quais não respondem conceitualmente, sobrepondo-os com exceções:
class Ave:
def voar(self): pass
class Pinguim(Ave):
def voar(self):
raise NotImplementedError("Pinguins não voam!") # Quebra o LSP!
Qualquer código que iterar sobre uma lista polimórfica List[Ave] e invocar .voar() colapsará em tempo de execução com exceção não tratada. A solução arquitetural correta é segregar as interfaces (Ave genérica vs. AveVoadora).
📊 5. Matriz Comparativa: SRP, OCP e LSP
| Princípio SOLID | Pergunta Central de Auditoria | Anti-Padrão Típico | Técnica Corretiva Principal |
|---|---|---|---|
| SRP (Responsabilidade Única) | Quantos atores de negócio distintos podem exigir alterações neste arquivo? | God Class / Blob, arquivos gigantescos com lógica mista de UI, DB e negócio | Decomposição em classes de domínio coesas e serviços orquestradores |
| OCP (Aberto / Fechado) | Para adicionar uma nova variação comercial, preciso alterar código já testado? | Blocos massivos de switch / if-else inspecionando enums de tipo |
Criação de abstrações/interfaces e injeção de dependência polimórfica |
| LSP (Substituição de Liskov) | Posso substituir a classe base pela subclasse em qualquer chamada sem quebras? | Métodos sobrepostos com raise NotImplementedError, checagens com isinstance |
Segregação de hierarquias de herança e respeito estrito ao Design by Contract |