Wilberhg's blog

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 S é um subtipo de T, objetos do tipo T devem poder ser substituídos por objetos do tipo S 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

  1. 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.
  2. OCP: Em vez de blocos gigantescos de match/case ou if/elif, use classes filhas ou estratégias polimórficas.
  3. LSP: Nunca lance NotImplementedError ou silencie métodos em subclasses só porque o pai exigiu algo que a filha não faz.
  4. ISP: Prefira vários Protocols curtos com 1 a 3 métodos a uma classe base com 15 métodos abstratos.
  5. 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.

#automation #backend #dev #pattern #python #solid #structure