Tutorial Prático: Princípios SOLID em Python
O acrônimo SOLID reúne cinco princípios de design orientado a objetos introduzidos por Robert C. Martin (Uncle Bob). O objetivo central do SOLID é construir softwares mais manuteníveis, flexíveis, testáveis e fáceis de evoluir.
Em linguagens dinâmicas como o Python, muitos desenvolvedores acreditam erroneamente que o SOLID não se aplica. No entanto, o ecossistema moderno do Python (com Type Hints, abc.ABC e typing.Protocol) torna a aplicação do SOLID essencial para arquiteturas corporativas robustas.
Sumário dos Princípios
| Letra | Princípio | Em poucas palavras |
|---|---|---|
| S | Single Responsibility Principle (SRP) | Uma classe deve ter apenas um motivo para mudar. |
| O | Open/Closed Principle (OCP) | Aberto para extensão, fechado para modificação. |
| L | Liskov Substitution Principle (LSP) | Subtipos devem ser substituíveis por seus tipos base sem quebrar o sistema. |
| I | Interface Segregation Principle (ISP) | Nenhuma classe deve ser forçada a depender de métodos que não usa. |
| D | Dependency Inversion Principle (DIP) | Dependa de abstrações, não de implementações concretas. |
1. Single Responsibility Principle (SRP)
"Uma classe deve ter um, e apenas um, motivo para mudar."
Uma classe/módulo deve focar em fazer uma única coisa bem feita. Misturar regras de negócio, persistência em banco de dados e envio de e-mails em um mesmo componente cria um acoplamento perigoso.
❌ Violação (Anti-pattern: Deus/Fat Class)
class ProcessadorPedido:
def processar(self, pedido_data: dict) -> None:
# 1. Regra de negócio
total = sum(item["preco"] * item["qtd"] for item in pedido_data["itens"])
print(f"Total calculado: R$ {total:.2f}")
# 2. Persistência de dados
import sqlite3
conn = sqlite3.connect("loja.db")
cursor = conn.cursor()
cursor.execute("INSERT INTO pedidos (total) VALUES (?)", (total,))
conn.commit()
# 3. Notificação externa
import smtplib
print(f"Enviando e-mail de confirmação para {pedido_data['cliente_email']}...")
Problema: Se a forma de salvar mudar (ex.: de SQLite para PostgreSQL), a classe muda. Se o provedor de e-mail mudar, a classe muda. Se o cálculo de impostos mudar, a classe muda.
✅ Solução Refatorada com SRP
Dividimos as responsabilidades em componentes coesos:
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Item:
nome: str
preco: float
quantidade: int
@dataclass
class Pedido:
cliente_email: str
itens: List[Item]
@property
def total(self) -> float:
return sum(item.preco * item.quantidade for item in self.itens)
class PedidoRepository:
"""Responsável unicamente pela persistência do pedido."""
def salvar(self, pedido: Pedido) -> None:
print(f"[DB] Pedido salvo com total de R$ {pedido.total:.2f}")
class ServicoEmail:
"""Responsável unicamente por comunicações e notificações."""
def notificar_cliente(self, email: str, total: float) -> None:
print(f"[Email] Enviando comprovante de R$ {total:.2f} para {email}")
class CheckoutService:
"""Orquestrador do caso de uso de checkout."""
def __init__(self, repository: PedidoRepository, email_service: ServicoEmail):
self.repository = repository
self.email_service = email_service
def finalizar(self, pedido: Pedido) -> None:
self.repository.salvar(pedido)
self.email_service.notificar_cliente(pedido.cliente_email, pedido.total)
2. Open/Closed Principle (OCP)
"Entidades de software devem ser abertas para extensão, mas fechadas para modificação."
Você deve ser capaz de adicionar um novo comportamento ao sistema sem precisar alterar código existente que já funciona e foi testado.
❌ Violação (Cadeia infinita de if/elif)
class CalculadoraDesconto:
def calcular(self, tipo_cliente: str, valor: float) -> float:
if tipo_cliente == "comum":
return valor
elif tipo_cliente == "vip":
return valor * 0.90 # 10% de desconto
elif tipo_cliente == "funcionario":
return valor * 0.70 # 30% de desconto
# Toda vez que criamos uma nova categoria, precisamos ALTERAR esta classe!
raise ValueError("Tipo de cliente desconhecido")
✅ Solução Refatorada com OCP (Estratégia/Polimorfismo)
Utilizamos interfaces abstratas (abc.ABC) ou classes base para permitir extensões plugáveis.
from abc import ABC, abstractmethod
class RegraDesconto(ABC):
@abstractmethod
def aplicar(self, valor: float) -> float:
pass
class DescontoComum(RegraDesconto):
def aplicar(self, valor: float) -> float:
return valor
class DescontoVIP(RegraDesconto):
def aplicar(self, valor: float) -> float:
return valor * 0.90
class DescontoFuncionario(RegraDesconto):
def aplicar(self, valor: float) -> float:
return valor * 0.70
# Para adicionar um novo desconto (ex: Black Friday), apenas criamos uma nova classe!
class DescontoBlackFriday(RegraDesconto):
def aplicar(self, valor: float) -> float:
return valor * 0.50
class CalculadoraDesconto:
"""Fechada para modificação, aberta para novos tipos de RegraDesconto."""
def calcular(self, regra: RegraDesconto, valor: float) -> float:
return regra.aplicar(valor)
3. Liskov Substitution Principle (LSP)
"Se é um subtipo de , objetos do tipo devem poder ser substituídos por objetos do tipo sem quebrar o funcionamento do programa."
Subclasses não devem violar os contratos nem enfraquecer pré-condições da classe base. Se uma subclasse levanta exceções inesperadas para métodos herdados ou muda o comportamento esperado, ela quebra o LSP.
❌ Violação (O clássico exemplo do Pássaro)
class Ave:
def voar(self) -> str:
return "Voando pelo céu..."
class Gaviao(Ave):
pass
class Pinguim(Ave):
def voar(self) -> str:
# Pinguins não voam! Quebra a expectativa de quem chama ave.voar()
raise NotImplementedError("Pinguins não conseguem voar!")
def realizar_voo_coletivo(aves: list[Ave]):
for ave in aves:
# Irá falhar em tempo de execução ao encontrar um Pinguim!
print(ave.voar())
✅ Solução Refatorada com LSP
Crie abstrações que representem com precisão as capacidades dos objetos:
from abc import ABC, abstractmethod
class Ave(ABC):
@abstractmethod
def emitir_som(self) -> str:
pass
class AveQueVoa(Ave):
@abstractmethod
def voar(self) -> str:
pass
class Gaviao(AveQueVoa):
def emitir_som(self) -> str:
return "Grito de gavião"
def voar(self) -> str:
return "Voando alto em correntes térmicas"
class Pinguim(Ave):
def emitir_som(self) -> str:
return "Grasnado de pinguim"
def decolar_esquadrao(aves: list[AveQueVoa]):
for ave in aves:
print(ave.voar()) # Seguro: garantido que todos os itens sabem voar
4. Interface Segregation Principle (ISP)
"Muitas interfaces específicas são melhores do que uma interface geral."
Nenhum cliente deve ser forçado a implementar métodos que não utiliza. Em Python, podemos usar typing.Protocol (Duck Typing estrutural) ou abc.ABC pequenas e atômicas.
❌ Violação ("Interface Gorda")
from abc import ABC, abstractmethod
class DispositivoMultifuncional(ABC):
@abstractmethod
def imprimir(self, documento: str) -> None:
pass
@abstractmethod
def digitalizar(self) -> str:
pass
@abstractmethod
def enviar_fax(self, telefone: str) -> None:
pass
class ImpressoraEconomica(DispositivoMultifuncional):
def imprimir(self, documento: str) -> None:
print(f"Imprimindo {documento}")
def digitalizar(self) -> str:
raise NotImplementedError("Não tenho scanner!")
def enviar_fax(self, telefone: str) -> None:
raise NotImplementedError("Não tenho módulo de fax!")
✅ Solução Refatorada com ISP (Usando Protocols em Python)
Com typing.Protocol, criamos contratos enxutos e independentes:
from typing import Protocol
class Imprimivel(Protocol):
def imprimir(self, documento: str) -> None: ...
class Digitalizavel(Protocol):
def digitalizar(self) -> str: ...
# Uma impressora básica só precisa implementar o que faz:
class ImpressoraSimples:
def imprimir(self, documento: str) -> None:
print(f"[Impressora] Imprimindo: {documento}")
# Uma multifuncional de ponta implementa múltiplos contratos:
class SuperMultifuncional:
def imprimir(self, documento: str) -> None:
print(f"[Multi] Imprimindo: {documento}")
def digitalizar(self) -> str:
return "[Multi] Conteúdo digitalizado em PDF"
def rotina_de_impressao(dispositivo: Imprimivel, texto: str):
"""Depende exclusivamente da capacidade de imprimir."""
dispositivo.imprimir(texto)
rotina_de_impressao(ImpressoraSimples(), "Relatório Mensal")
rotina_de_impressao(SuperMultifuncional(), "Relatório Mensal")
5. Dependency Inversion Principle (DIP)
"Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações."
DIP desacopla a regra de negócio central da sua aplicação das ferramentas externas (bancos de dados, APIs de terceiros, sistemas de arquivos).
❌ Violação (Alto nível acoplado diretamente a ferramentas de baixo nível)
class PostgresConnection:
def query(self, sql: str) -> list:
print(f"Executando no Postgres: {sql}")
return [{"id": 1, "nome": "Maria"}]
class RelatorioFinanceiro:
def __init__(self):
# Acoplamento rígido! Se quisermos testar com mocks ou trocar para
# MySQL/MongoDB/Firestore, precisamos reescrever esta classe.
self.db = PostgresConnection()
def gerar(self) -> None:
dados = self.db.query("SELECT * FROM lancamentos")
print(f"Relatório gerado com {len(dados)} lançamentos.")
✅ Solução Refatorada com DIP (Inversão e Injeção de Dependências)
from typing import Protocol, List, Dict, Any
# 1. Abstração (contrato definido pelo módulo de alto nível ou domínio)
class DatabaseClient(Protocol):
def query(self, sql: str) -> List[Dict[str, Any]]: ...
# 2. Implementações de Baixo Nível (detalhes de infraestrutura)
class PostgresConnection:
def query(self, sql: str) -> List[Dict[str, Any]]:
print(f"[PostgreSQL] Executando: {sql}")
return [{"id": 1, "valor": 1500.0}]
class FirestoreConnection:
def query(self, sql: str) -> List[Dict[str, Any]]:
print(f"[Firestore] Consultando documentos...")
return [{"id": "doc_123", "valor": 2200.0}]
class MockDatabaseForTests:
def query(self, sql: str) -> List[Dict[str, Any]]:
return [{"id": 999, "valor": 100.0}]
# 3. Módulo de Alto Nível (recebe a dependência externamente - Injeção)
class RelatorioFinanceiro:
def __init__(self, db: DatabaseClient):
self.db = db
def gerar(self) -> float:
registros = self.db.query("SELECT * FROM lancamentos")
total = sum(item["valor"] for item in registros)
return total
# Uso em Produção com PostgreSQL:
relatorio_prod = RelatorioFinanceiro(db=PostgresConnection())
print(f"Total Prod: R$ {relatorio_prod.gerar():.2f}")
# Uso em Testes Unitários sem tocar em banco de dados real:
relatorio_teste = RelatorioFinanceiro(db=MockDatabaseForTests())
assert relatorio_teste.gerar() == 100.0
6. Onde os conceitos SOLID residem na estrutura do projeto?
Ao adotar padrões como Clean Architecture ou Hexagonal (Ports and Adapters), os princípios SOLID moldam a organização dos pacotes:
meu_projeto/
├── src/
│ ├── domain/ # NÚCLEO DO SISTEMA (Alto nível)
│ │ ├── entities/ # Modelos puros e regras de negócio (SRP)
│ │ │ ├── pedido.py
│ │ │ └── cliente.py
│ │ └── protocols/ # Interfaces / Contratos Abstratos (DIP, ISP, OCP)
│ │ ├── pedido_repository.py # Protocol/ABC do repositório
│ │ ├── notification_gateway.py # Protocol/ABC de notificações
│ │ └── discount_strategy.py # Protocol/ABC de estratégias
│ │
│ ├── application/ # CASOS DE USO (Orquestração)
│ │ └── use_cases/
│ │ └── processar_checkout.py # Depende apenas dos protocols do domínio (DIP)
│ │
│ └── infrastructure/ # DETALHES DE IMPLEMENTAÇÃO (Baixo nível)
│ ├── database/
│ │ ├── postgres_repository.py # Implementa PedidoRepositoryProtocol
│ │ └── firestore_repository.py # Alternativa plugável (LSP, DIP)
│ ├── notifications/
│ │ └── sendgrid_email.py # Implementa NotificationGateway
│ └── strategies/
│ ├── black_friday.py # Implementa DiscountStrategy (OCP)
│ └── vip_discount.py
│
├── tests/
│ ├── unit/ # Testes isolados com mocks/stubs rápidos (facilitados pelo DIP)
│ └── integration/
├── pyproject.toml
└── main.py # Ponto de composição (Composition Root onde as injeções ocorrem)
Resumo Prático para o Dia a Dia
- SRP: Se ao descrever sua classe você usa a conjunção "e" com frequência ("Esta classe calcula o imposto E grava no banco E manda WhatsApp"), fatore-a em classes menores.
- OCP: Em vez de blocos gigantescos de
match/caseouif/elif, use classes filhas ou estratégias polimórficas. - LSP: Nunca lance
NotImplementedErrorou silencie métodos em subclasses só porque o pai exigiu algo que a filha não faz. - ISP: Prefira vários
Protocols curtos com 1 a 3 métodos a uma classe base com 15 métodos abstratos. - DIP: Nunca instancie classes de infraestrutura pesadas (conexões de rede, banco, APIs) dentro do
__init__das suas classes de negócio. Receba-as como parâmetros.