DEV Community

Cover image for Reason Your Python Code is Hard to Test with Single Responsibility Principle, And How to Fix It! (Portuguese version)
Cláudio Filipe Lima Rapôso
Cláudio Filipe Lima Rapôso

Posted on

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 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}...")


Enter fullscreen mode Exit fullscreen mode

Uso

service = UserService()
service.register("Alice", "alice@example.com", "123456")
Enter fullscreen mode Exit fullscreen mode

✅ 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

Enter fullscreen mode Exit fullscreen mode

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")

Enter fullscreen mode Exit fullscreen mode

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}')")

Enter fullscreen mode Exit fullscreen mode

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.")

Enter fullscreen mode Exit fullscreen mode

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}")

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)