Dev (Back & Front)ARTIGO

Motivos pelos quais seu código Python é difícil de testar com o Princípio da Responsabilidade Única e como corrigir isso!

Motivos pelos quais seu código Python é difícil de testar com o Princípio da Responsabilidade Única e como corrigir isso!
Imagem: Cláudio Raposo

O Princípio da Responsabilidade Única (SRP, na sigla em inglês) é a base dos princípios SOLID. Ele afirma que uma classe deve ter um, e apenas um, motivo para ser alterada.

Quando ignoramos esse princípio, criamos componentes “Classes Deus” que sabem demais e fazem demais. Essas classes são frágeis: uma alteração no banco de dadosBanco de dados134 conteúdosSQL ou NoSQL: eis a questão!!Data · mar 2020Banco de dados: como organizar e dar segurança para milhões de dados de loteriasData · mai 20215 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data pode quebrar a lógica de validação, ou uma mudança nas regras de negócio pode quebrar o sistema de notificação por e-mail.

Neste guia, analisaremos um antipadrão comum em um recurso de registro de usuários e o refatoraremos em 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á se comportando como um serviço; está se comportando como um monolito.

Por que isso é ruim:

  • Alto acoplamento: Está fortemente acoplado a regras de validação específicas e instruções de impressão específicas do banco de dados.
  • Difícil de testar: Não é possível testar a lógica de registro sem acionar a lógica do banco de dados.
  • Vários motivos para alteração: Este arquivo será aberto se as regras de senha forem alteradas. Este arquivo será aberto se o banco de dados for alterado.
class  UserService : </code>
<code>    def  register ( self, name, email, password ): </code>
<code>        # Responsabilidade 1: Lógica de Validação </code>
<code>        if  len (name) < 3 : </code>
<code>            raise ValueError( "Nome muito curto" ) </code>
<code>        if  "@"  not  in email: </code>
<code>            raise ValueError( "Email inválido" ) </code>
<code>        if  len (password) < 6 : </code>
<code>            raise ValueError( "Senha muito fraca" ) </code>
<code>        # Responsabilidade 2: Lógica do Banco de Dados (Verificar existência) </code>
<code>        print ( f"[DB] Verificando se {email} existe..." ) </code>
<code>        if email == "taken@example.com" : </code>
<code>             raise ValueError( "Email já em uso" ) </code>
<code>        # Responsabilidade 3: Lógica de Persistência (Salvar) </code>
<code>        print ( f"[DB] INSERT INTO users (name, email) VALUES (' {name} ', ' {email} ')" ) </code>

<code>        # Responsabilidade 4: Lógica de Notificação </code>
<code>        print ( f"[MAIL] Enviando e-mail de boas-vindas para {email} ..." )</code>
</pre>
<p><strong>Uso:</strong></p>
<pre class="rh ri rj rk rl rm qx rn hv ro bv cc"><code>#
 Serviço de uso = UserService() 
service.register( "Alice" , "alice@example.com" , "123456" )

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

Para corrigir isso, precisamos identificar as responsabilidades distintas e separá-las em classes próprias.

1. A Entidade (Estrutura de Dados)

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

  • Motivo da alteração: Somente se phone_numberforem adicionados atributos de usuário (como ).
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 da alteração: Somente se as regras para dados válidos forem alteradas.
class  UserValidator : 
    def  validate ( self, user: User ): 
        if  len (user.name) < 3 : 
            raise ValueError( "Nome muito curto" ) 
        if  "@"  not  in user.email: 
            raise ValueError( "Email inválido" ) 
        if  len (user.password) < 6 : 
            raise ValueError( "Senha muito fraca" )

3. O Repositório (Persistência)

Abstraímos as operações do banco de dados. Esta classe lida com o “como” armazenar os dados.

  • Motivo para alteração: Somente se a tecnologia do banco de dados (SQL, NoSQL, Arquivo) for alterada.
class  UserRepository : 
    def  exists ( self, email: str ) -> bool : 
        # Simula verificação no banco de dados 
        return email == "taken@example.com" 
    def  save ( self, user: User ): 
        # Simula salvamento no banco de dados 
        print ( f"[DB] INSERT INTO users VALUES (' {user.name} ', ' {user.email} ')" )

4. O Serviço (O Orquestrador)

Finalmente, reescrevemos o código UserService. Observe como ele ficou limpo. Ele não sabe como validar ou salvar ; simplesmente delega essas tarefas aos especialistas (as dependências injetadas).

  • Motivo da alteração: Somente se o fluxo de trabalho do cadastro for alterado (por exemplo, adicionando uma etapa para verificar o número de telefone via SMS).
class  UserService : 
    def  __init__ ( self, validator: UserValidator, repository: UserRepository ): 
        # Injeção de Dependência: Solicitamos as ferramentas necessárias 
        self.validator = validator 
        self.repository = repository 
    def  register ( self, name, email, password ): 
        user = User(name, email, password) 
        # Etapa 1: Validar 
        self.validator.validate (user) 
        # Etapa 2: Verificar as Regras de Negócio 
        if  self.repository.exists (email): 
            raise ValueError( "Email já está em uso" ) 
        # Etapa 3: Persistir 
        self.repository.save (user) 
        print ( ">">> Usuário registrado com sucesso." )

Exemplo de uso

Ao interligar os componentes, criamos um sistema flexível.

    # 1. Configurar as dependências
     repo = UserRepository() 
    val = UserValidator() 
    
    # 2. Injetar as dependências no serviço
     service = UserService(validator=val, repository=repo) 
    
    # 3. Executar a lógica 
    try : 
        service.register( "Bob" , "bob@example.com" , "securePass123" ) 
    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:

  • Testabilidade: Agora você pode realizar testes unitários UserValidatorisoladamente, sem a necessidade de um banco de dados falso.
  • Facilidade de manutenção: Se o esquema do banco de dados for alterado, você só precisa editar o arquivo UserRepository. O UserServicerestante permanece intacto.
  • Legibilidade: O UserServicetexto agora se apresenta como uma narrativa concisa dos acontecimentos , em vez de uma lista confusa de como eles acontecem.

Essa separação reduz a carga cognitiva do desenvolvedor e garante que seu aplicativo seja robusto o suficiente para lidar com mudanças futuras com risco mínimo.

Systems Architect Project Leader @NTT DATA👨‍💻 | 2y Microsoft MVP® Developer Technologies 🏆 | 1y Microsoft MVP Azure® 🏆 | 2y Docker Captain® 🏆 | Undergraduate Teacher @Moveedu 👨‍🏫 | 🇺🇸 DBA, Hon.D.Sc., MCS, MBA

Ver perfil