Python Admin

Clean Architecture em Python: Estruturando Projetos para Escalar na Prática Já leu

Clean Architecture em Python: Estruturando Projetos para Escalar A Clean Architecture, conceito popularizado por Robert C. Martin, é um padrão de design que coloca a lógica de negócio no centro da aplicação, isolando-a de detalhes técnicos como frameworks, bancos de dados e interfaces de usuário. Em Python, aplicar esses princípios significa organizar seu projeto em camadas concêntricas onde cada uma tem responsabilidades bem definidas e depende apenas das camadas internas. Quando você estrutura um projeto seguindo Clean Architecture, ganha capacidade de manutenção, testabilidade e flexibilidade para trocar tecnologias sem afetar o núcleo da aplicação. Um projeto que começa pequeno pode crescer significativamente, e uma base sólida arquitetural economiza horas de refatoração futura. Neste artigo, vamos explorar como implementar esses conceitos de forma prática em Python. As Camadas da Clean Architecture Entendendo a Estrutura em Camadas Clean Architecture é frequentemente representada como círculos concêntricos. De fora para dentro, temos: Frameworks & Drivers (camada mais externa), Interface Adapters, Application Business Rules e

Clean Architecture em Python: Estruturando Projetos para Escalar

A Clean Architecture, conceito popularizado por Robert C. Martin, é um padrão de design que coloca a lógica de negócio no centro da aplicação, isolando-a de detalhes técnicos como frameworks, bancos de dados e interfaces de usuário. Em Python, aplicar esses princípios significa organizar seu projeto em camadas concêntricas onde cada uma tem responsabilidades bem definidas e depende apenas das camadas internas.

Quando você estrutura um projeto seguindo Clean Architecture, ganha capacidade de manutenção, testabilidade e flexibilidade para trocar tecnologias sem afetar o núcleo da aplicação. Um projeto que começa pequeno pode crescer significativamente, e uma base sólida arquitetural economiza horas de refatoração futura. Neste artigo, vamos explorar como implementar esses conceitos de forma prática em Python.

As Camadas da Clean Architecture

Entendendo a Estrutura em Camadas

Clean Architecture é frequentemente representada como círculos concêntricos. De fora para dentro, temos: Frameworks & Drivers (camada mais externa), Interface Adapters, Application Business Rules e Entity Business Rules (camada mais interna). A regra fundamental é que as dependências sempre apontam para dentro — a camada mais interna nunca conhece a mais externa.

Em um projeto Python real, estruturamos isso em pacotes e módulos. A camada mais interna contém as entidades (modelos de domínio puros), seguida pela camada de casos de uso (application services), depois os adaptadores (controllers, presenters) e por fim os frameworks e detalhes técnicos.

Estrutura de Diretórios Recomendada

meu_projeto/
├── src/
│   ├── dominio/                 # Camada de negócio puro
│   │   ├── entidades.py
│   │   └── value_objects.py
│   ├── casos_uso/               # Regras de negócio da aplicação
│   │   ├── criar_usuario.py
│   │   └── autenticar_usuario.py
│   ├── adaptadores/             # Interface Adapters
│   │   ├── controllers/
│   │   ├── presenters/
│   │   └── gateways/
│   ├── frameworks/              # Detalhes técnicos (Flask, BD, etc)
│   │   ├── web/
│   │   ├── persistencia/
│   │   └── config.py
│   └── __init__.py
├── tests/
├── requirements.txt
└── README.md

Implementando a Camada de Domínio

Entidades e Value Objects

A camada de domínio contém as entidades — objetos que representam conceitos do seu negócio — e value objects — objetos imutáveis que descrevem características. Eles não sabem nada sobre banco de dados, HTTP ou qualquer detalhe técnico. São puros.

Uma entidade tem identidade única e ciclo de vida. Um value object não tem identidade; dois value objects com os mesmos atributos são equivalentes. Vamos ver um exemplo prático:

# src/dominio/entidades.py
from datetime import datetime
from typing import Optional

class Usuario:
    """Entidade de domínio que representa um usuário."""

    def __init__(
        self, 
        id: str, 
        nome: str, 
        email: str, 
        senha_hash: str,
        data_criacao: Optional[datetime] = None
    ):
        self.id = id
        self.nome = nome
        self.email = email
        self.senha_hash = senha_hash
        self.data_criacao = data_criacao or datetime.now()
        self.ativo = True

    def desativar(self) -> None:
        """Ação de negócio: desativar usuário."""
        self.ativo = False

    def atualizar_nome(self, novo_nome: str) -> None:
        """Ação de negócio: atualizar nome."""
        if not novo_nome or len(novo_nome) < 3:
            raise ValueError("Nome deve ter pelo menos 3 caracteres")
        self.nome = novo_nome
# src/dominio/value_objects.py
from dataclasses import dataclass
import re

@dataclass(frozen=True)
class Email:
    """Value object imutável para email."""
    endereco: str

    def __post_init__(self):
        if not self._validar():
            raise ValueError(f"Email inválido: {self.endereco}")

    def _validar(self) -> bool:
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return re.match(pattern, self.endereco) is not None

    def __str__(self) -> str:
        return self.endereco

Repare que a entidade Usuario contém lógica de negócio (validar nome, desativar). O value object Email encapsula a validação de email. Nenhum deles importa qualquer framework ou detalhe técnico.

Casos de Uso e Portas

Definindo Casos de Uso (Interactors)

Um caso de uso representa uma funcionalidade de negócio que a aplicação executa. Ele orquestra entidades e repositories (que são abstrações, não implementações). Os casos de uso devem ser independentes de frameworks e tecnologias específicas.

# src/casos_uso/criar_usuario.py
from abc import ABC, abstractmethod
from src.dominio.entidades import Usuario
from src.dominio.value_objects import Email

class RepositorioUsuario(ABC):
    """Porto (interface) para persistência de usuários."""

    @abstractmethod
    def salvar(self, usuario: Usuario) -> None:
        pass

    @abstractmethod
    def obter_por_email(self, email: str) -> Usuario | None:
        pass

class GeradorIdUsuario(ABC):
    """Porto para gerar IDs."""

    @abstractmethod
    def gerar(self) -> str:
        pass

class HashearSenha(ABC):
    """Porto para hashing de senhas."""

    @abstractmethod
    def hashear(self, senha: str) -> str:
        pass

class CriarUsuarioUseCase:
    """Caso de uso: criar um novo usuário."""

    def __init__(
        self,
        repositorio: RepositorioUsuario,
        gerador_id: GeradorIdUsuario,
        hasheador: HashearSenha
    ):
        self.repositorio = repositorio
        self.gerador_id = gerador_id
        self.hasheador = hasheador

    def executar(self, nome: str, email: str, senha: str) -> Usuario:
        """
        Executa a lógica de negócio para criar um usuário.

        Exceções levantadas aqui são exceções de negócio,
        não técnicas.
        """
        # Validar email (value object faz isso)
        email_validado = Email(email)

        # Verificar se usuário já existe
        usuario_existente = self.repositorio.obter_por_email(email)
        if usuario_existente:
            raise ValueError(f"Usuário com email {email} já existe")

        # Criar usuário
        novo_usuario = Usuario(
            id=self.gerador_id.gerar(),
            nome=nome,
            email=str(email_validado),
            senha_hash=self.hasheador.hashear(senha)
        )

        # Persistir
        self.repositorio.salvar(novo_usuario)

        return novo_usuario

Observe que o caso de uso não sabe como a senha é hashada, como o ID é gerado ou como o usuário é armazenado. Ele depende de abstrações (portos), permitindo diferentes implementações.

Adaptadores e Frameworks

Implementando os Adaptadores

Os adaptadores convertem dados entre o mundo externo (web, CLI, mensagens) e o mundo interno (casos de uso). Aqui implementamos as portas definidas nos casos de uso.

# src/adaptadores/gateways/repositorio_usuario_sqlite.py
import sqlite3
import json
from src.casos_uso.criar_usuario import RepositorioUsuario
from src.dominio.entidades import Usuario
from datetime import datetime

class RepositorioUsuarioSQLite(RepositorioUsuario):
    """Implementação de persistência com SQLite."""

    def __init__(self, caminho_db: str):
        self.caminho_db = caminho_db
        self._inicializar_db()

    def _inicializar_db(self):
        with sqlite3.connect(self.caminho_db) as conn:
            conn.execute('''
                CREATE TABLE IF NOT EXISTS usuarios (
                    id TEXT PRIMARY KEY,
                    nome TEXT NOT NULL,
                    email TEXT UNIQUE NOT NULL,
                    senha_hash TEXT NOT NULL,
                    data_criacao TEXT NOT NULL,
                    ativo INTEGER NOT NULL
                )
            ''')
            conn.commit()

    def salvar(self, usuario: Usuario) -> None:
        with sqlite3.connect(self.caminho_db) as conn:
            conn.execute('''
                INSERT OR REPLACE INTO usuarios 
                (id, nome, email, senha_hash, data_criacao, ativo)
                VALUES (?, ?, ?, ?, ?, ?)
            ''', (
                usuario.id,
                usuario.nome,
                usuario.email,
                usuario.senha_hash,
                usuario.data_criacao.isoformat(),
                1 if usuario.ativo else 0
            ))
            conn.commit()

    def obter_por_email(self, email: str) -> Usuario | None:
        with sqlite3.connect(self.caminho_db) as conn:
            cursor = conn.execute(
                'SELECT id, nome, email, senha_hash, data_criacao FROM usuarios WHERE email = ?',
                (email,)
            )
            linha = cursor.fetchone()

        if not linha:
            return None

        return Usuario(
            id=linha[0],
            nome=linha[1],
            email=linha[2],
            senha_hash=linha[3],
            data_criacao=datetime.fromisoformat(linha[4])
        )
# src/adaptadores/gateways/geradores.py
import uuid
from src.casos_uso.criar_usuario import GeradorIdUsuario, HashearSenha
import hashlib

class GeradorIdUUID(GeradorIdUsuario):
    """Gera IDs usando UUID."""

    def gerar(self) -> str:
        return str(uuid.uuid4())

class HashearSenhaSimples(HashearSenha):
    """Hasheador de senhas com SHA-256 (use bcrypt em produção!)."""

    def hashear(self, senha: str) -> str:
        return hashlib.sha256(senha.encode()).hexdigest()
# src/adaptadores/controllers/usuario_controller.py
from src.casos_uso.criar_usuario import CriarUsuarioUseCase
from src.adaptadores.presenters.usuario_presenter import UsuarioPresenter

class UsuarioController:
    """Controller para requisições de usuário (via HTTP, CLI, etc)."""

    def __init__(self, caso_uso: CriarUsuarioUseCase, presenter: UsuarioPresenter):
        self.caso_uso = caso_uso
        self.presenter = presenter

    def criar_usuario(self, dados: dict) -> dict:
        """
        Recebe dados do cliente (desserializados de JSON, por exemplo)
        e retorna resposta formatada.
        """
        try:
            usuario = self.caso_uso.executar(
                nome=dados['nome'],
                email=dados['email'],
                senha=dados['senha']
            )
            return self.presenter.usuario_criado(usuario)
        except ValueError as e:
            return self.presenter.erro_validacao(str(e))

Presenter e Formatação de Resposta

# src/adaptadores/presenters/usuario_presenter.py
from src.dominio.entidades import Usuario
from datetime import datetime

class UsuarioPresenter:
    """Presenter que formata dados de domínio para resposta HTTP/API."""

    def usuario_criado(self, usuario: Usuario) -> dict:
        return {
            'status': 'sucesso',
            'dados': {
                'id': usuario.id,
                'nome': usuario.nome,
                'email': usuario.email,
                'data_criacao': usuario.data_criacao.isoformat(),
                'ativo': usuario.ativo
            }
        }

    def erro_validacao(self, mensagem: str) -> dict:
        return {
            'status': 'erro',
            'mensagem': mensagem
        }

Montando Tudo com Dependency Injection

Factory e Container de Injeção

# src/frameworks/container.py
from src.casos_uso.criar_usuario import (
    CriarUsuarioUseCase,
    RepositorioUsuario,
    GeradorIdUsuario,
    HashearSenha
)
from src.adaptadores.gateways.repositorio_usuario_sqlite import RepositorioUsuarioSQLite
from src.adaptadores.gateways.geradores import GeradorIdUUID, HashearSenhaSimples
from src.adaptadores.controllers.usuario_controller import UsuarioController
from src.adaptadores.presenters.usuario_presenter import UsuarioPresenter

class Container:
    """Container de injeção de dependência."""

    def __init__(self, caminho_db: str = 'app.db'):
        self._repositorio_usuario: RepositorioUsuario = RepositorioUsuarioSQLite(caminho_db)
        self._gerador_id: GeradorIdUsuario = GeradorIdUUID()
        self._hasheador: HashearSenha = HashearSenhaSimples()

    def criar_usuario_use_case(self) -> CriarUsuarioUseCase:
        return CriarUsuarioUseCase(
            repositorio=self._repositorio_usuario,
            gerador_id=self._gerador_id,
            hasheador=self._hasheador
        )

    def usuario_controller(self) -> UsuarioController:
        return UsuarioController(
            caso_uso=self.criar_usuario_use_case(),
            presenter=UsuarioPresenter()
        )

Exemplo de Uso com Flask

# src/frameworks/web/app.py
from flask import Flask, request, jsonify
from src.frameworks.container import Container

app = Flask(__name__)
container = Container()

@app.route('/usuarios', methods=['POST'])
def criar_usuario():
    dados = request.get_json()
    controller = container.usuario_controller()
    resultado = controller.criar_usuario(dados)

    status_code = 201 if resultado['status'] == 'sucesso' else 400
    return jsonify(resultado), status_code

if __name__ == '__main__':
    app.run(debug=True)

Testando com Clean Architecture

Testes Unitários sem Dependências Externas

# tests/test_criar_usuario_use_case.py
import unittest
from unittest.mock import Mock
from src.casos_uso.criar_usuario import CriarUsuarioUseCase
from src.dominio.entidades import Usuario

class MockRepositorio:
    def __init__(self):
        self.usuarios = {}

    def salvar(self, usuario: Usuario) -> None:
        self.usuarios[usuario.id] = usuario

    def obter_por_email(self, email: str) -> Usuario | None:
        for usuario in self.usuarios.values():
            if usuario.email == email:
                return usuario
        return None

class MockGerador:
    def gerar(self) -> str:
        return "id_teste_123"

class MockHasheador:
    def hashear(self, senha: str) -> str:
        return f"hash({senha})"

class TestCriarUsuarioUseCase(unittest.TestCase):

    def setUp(self):
        self.repositorio = MockRepositorio()
        self.gerador = MockGerador()
        self.hasheador = MockHasheador()
        self.caso_uso = CriarUsuarioUseCase(
            self.repositorio,
            self.gerador,
            self.hasheador
        )

    def test_criar_usuario_com_sucesso(self):
        usuario = self.caso_uso.executar(
            nome="João Silva",
            email="joao@example.com",
            senha="senha123"
        )

        self.assertEqual(usuario.nome, "João Silva")
        self.assertEqual(usuario.email, "joao@example.com")
        self.assertTrue(usuario.ativo)

    def test_email_invalido_levanta_excecao(self):
        with self.assertRaises(ValueError):
            self.caso_uso.executar(
                nome="João Silva",
                email="email_invalido",
                senha="senha123"
            )

    def test_usuario_duplicado_levanta_excecao(self):
        self.caso_uso.executar(
            nome="João Silva",
            email="joao@example.com",
            senha="senha123"
        )

        with self.assertRaises(ValueError):
            self.caso_uso.executar(
                nome="Outro João",
                email="joao@example.com",
                senha="outra_senha"
            )

if __name__ == '__main__':
    unittest.main()

Veja que os testes não usam banco de dados real nem qualquer framework. São testes de lógica de negócio pura.

Conclusão

Clean Architecture em Python oferece três benefícios fundamentais que você sentirá imediatamente: isolamento de lógica de negócio torna seus testes simples e rápidos — você testa regras de negócio sem infra; flexibilidade para trocar tecnologias permite mudar de SQLite para PostgreSQL ou de Flask para FastAPI sem tocar em uma única linha de caso de uso; comunicação clara entre camadas via portos e adaptadores mantém o projeto organizado mesmo quando cresce de 10 para 100 mil linhas.

Não é necessário ser obsessivo — alguns projetos simples não precisam dessa estrutura completa. Mas quando seu projeto escala, essa arquitetura economiza refatorações massivas. Comece aplicando em um novo projeto e sinta como a organização facilita manutenção, testes e comunicação com seu time.

Referências


Artigos relacionados