4 min de leituraPortuguês

Reason Your Python Code is Hard to Test with Single Responsibility Principle, And How to Fix It! (Portuguese version)

O Princípio da Responsabilidade Única (SRP - Single Responsibility Principle) é a base dos princípios...

  • #phyton
  • #programming
  • #solidprinciples
  • #singleresponsibility

Publicado originalmente no dev.to (abre em nova aba) por Cláudio Rapôso. Comentários e reações ficam por lá.

O Princípio da Responsabilidade Única (SRP - Single Responsibility Principle) é a base dos princípios SOLID. Ele estabelece que uma classe deve ter um, e apenas um, motivo para mudar.

Quando ignoramos esse princípio, criamos "God Classes" (Classes Deus) componentes que sabem demais e fazem demais. Essas classes são frágeis: uma mudança no banco de dados pode quebrar a lógica de validação, ou uma alteração nas regras de negócio pode quebrar o sistema de envio de e-mails.

Neste guia, analisaremos um antipadrão comum em uma funcionalidade de Cadastro de Usuário e a refatoraremos para uma arquitetura limpa e desacoplada.


❌ O "Antes": A Violação do SRP

Aqui está um exemplo típico de código legado. Temos uma classe chamada UserService, mas ela não está agindo como um serviço; ela age como um monolito.

Por que isso é ruim:

  • Alto Acoplamento: Ela está fortemente ligada a regras de validação específicas e instruções de banco de dados (print).
  • Difícil de Testar: Você não consegue testar a lógica de registro sem acionar a lógica do banco de dados.
  • Múltiplos Motivos para Mudar: Se as regras de senha mudarem, este arquivo é aberto. Se o banco de dados mudar, este arquivo também é aberto.
class UserService:
    def register(self, name, email, password):
        # Responsabilidade 1: Lógica de Validação
        if len(name) < 3:
            raise ValueError("O nome é muito curto")
        if "@" not in email:
            raise ValueError("Email inválido")
        if len(password) < 6:
            raise ValueError("A senha é muito fraca")

        # Responsabilidade 2: Lógica de Banco de Dados (Verificar existência)
        print(f"[DB] Verificando se {email} existe...")
        if email == "taken@example.com":
             raise ValueError("Email já está em uso")

        # Responsabilidade 3: Lógica de Persistência (Salvar)
        print(f"[DB] INSERT INTO users (name, email) VALUES ('{name}', '{email}')")

        # Responsabilidade 4: Lógica de Notificação
        print(f"[MAIL] Enviando e-mail de boas-vindas para {email}...")


Uso

service = UserService()
service.register("Alice", "alice@example.com", "123456")

✅ A Refatoração: Separação de Preocupações

Para corrigir isso, devemos identificar as responsabilidades distintas e extraí-las para suas próprias classes.

1. A Entidade (Estrutura de Dados)

Primeiro, criamos uma representação dos dados. Esta classe não tem lógica; ela apenas armazena estado.

  • Motivo para mudar: Apenas se os atributos do usuário (como adicionar telefone) mudarem.
from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    password: str

2. O Validador (Regras de Negócio)

Extraímos as regras de validação. Esta classe garante a integridade dos dados.

  • Motivo para mudar: Apenas se as regras para dados válidos mudarem.
class UserValidator:
    def validate(self, user: User):
        if len(user.name) < 3:
            raise ValueError("O nome é muito curto")
        if "@" not in user.email:
            raise ValueError("Email inválido")
        if len(user.password) < 6:
            raise ValueError("A senha é muito fraca")

3. O Repositório (Persistência)

Abstraímos as operações de banco de dados. Esta classe lida com o "Como" armazenar os dados.

  • Motivo para mudar: Apenas se a tecnologia do banco de dados (SQL, NoSQL, Arquivo) mudar.
class UserRepository:
    def exists(self, email: str) -> bool:
        # Simula verificação no DB
        return email == "taken@example.com"

    def save(self, user: User):
        # Simula salvamento no DB
        print(f"[DB] INSERT INTO users VALUES ('{user.name}', '{user.email}')")

4. O Serviço (O Orquestrador)

Finalmente, reescrevemos o UserService. Note como ele ficou limpo. Ele não sabe como validar ou como salvar; ele simplesmente delega essas tarefas para os especialistas (as dependências injetadas).

  • Motivo para mudar: Apenas se o fluxo de trabalho do cadastro mudar (ex: adicionar uma etapa para verificar o telefone via SMS).
class UserService:
    def __init__(self, validator: UserValidator, repository: UserRepository):
        # Injeção de Dependência: Pedimos as ferramentas que precisamos
        self.validator = validator
        self.repository = repository

    def register(self, name, email, password):
        user = User(name, email, password)

        # Passo 1: Validar
        self.validator.validate(user)

        # Passo 2: Verificar Regras de Negócio
        if self.repository.exists(email):
            raise ValueError("Email já está em uso")

        # Passo 3: Persistir
        self.repository.save(user)

        print(">>> Usuário registrado com sucesso.")


Exemplo de Uso

Ao conectar os componentes ("wiring"), criamos um sistema flexível.

    # 1. Configurar as dependências
    repo = UserRepository()
    val = UserValidator()

    # 2. Injetar dependências no Serviço
    service = UserService(validator=val, repository=repo)

    # 3. Executar a lógica
    try:
        service.register("Bob", "bob@example.com", "senhaSegura123")
    except ValueError as e:
        print(f"Erro: {e}")


Conclusão

Ao aplicar o Princípio da Responsabilidade Única, transformamos um script rígido em uma arquitetura de software profissional.

Os benefícios dessa abordagem são claros:

  1. Testabilidade: Agora você pode criar testes unitários para o UserValidator isoladamente, sem precisar de um banco de dados falso.
  2. Manutenibilidade: Se o esquema do banco de dados mudar, você edita apenas o UserRepository. O UserService permanece intocado.
  3. Legibilidade: O UserService agora lê como uma história de alto nível sobre o que acontece, em vez de uma lista bagunçada de como acontece.

Essa separação reduz a carga cognitiva sobre o desenvolvedor e garante que sua aplicação seja robusta o suficiente para lidar com mudanças futuras com risco mínimo.