<h2>Clean Architecture em Python: Estruturando Projetos para Escalar</h2>
<p>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.</p>
<p>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.</p>
<h2>As Camadas da Clean Architecture</h2>
<h3>Entendendo a Estrutura em Camadas</h3>
<p>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 <strong>as dependências sempre apontam para dentro</strong> — a camada mais interna nunca conhece a mais externa.</p>
<p>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.</p>
<h3>Estrutura de Diretórios Recomendada</h3>
<pre><code>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</code></pre>
<h2>Implementando a Camada de Domínio</h2>
<h3>Entidades e Value Objects</h3>
<p>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.</p>
<p>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:</p>
<pre><code class="language-python"># 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</code></pre>
<pre><code class="language-python"># 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</code></pre>
<p>Repare que a entidade <code>Usuario</code> contém lógica de negócio (validar nome, desativar). O value object <code>Email</code> encapsula a validação de email. Nenhum deles importa qualquer framework ou detalhe técnico.</p>
<h2>Casos de Uso e Portas</h2>
<h3>Definindo Casos de Uso (Interactors)</h3>
<p>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.</p>
<pre><code class="language-python"># 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</code></pre>
<p>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.</p>
<h2>Adaptadores e Frameworks</h2>
<h3>Implementando os Adaptadores</h3>
<p>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.</p>
<pre><code class="language-python"># 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])
)</code></pre>
<pre><code class="language-python"># 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()</code></pre>
<pre><code class="language-python"># 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))</code></pre>
<h3>Presenter e Formatação de Resposta</h3>
<pre><code class="language-python"># 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
}</code></pre>
<h2>Montando Tudo com Dependency Injection</h2>
<h3>Factory e Container de Injeção</h3>
<pre><code class="language-python"># 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()
)</code></pre>
<h3>Exemplo de Uso com Flask</h3>
<pre><code class="language-python"># 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)</code></pre>
<h2>Testando com Clean Architecture</h2>
<h3>Testes Unitários sem Dependências Externas</h3>
<pre><code class="language-python"># 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()</code></pre>
<p>Veja que os testes não usam banco de dados real nem qualquer framework. São testes de lógica de negócio pura.</p>
<h2>Conclusão</h2>
<p>Clean Architecture em Python oferece três benefícios fundamentais que você sentirá imediatamente: <strong>isolamento de lógica de negócio</strong> torna seus testes simples e rápidos — você testa regras de negócio sem infra; <strong>flexibilidade para trocar tecnologias</strong> permite mudar de SQLite para PostgreSQL ou de Flask para FastAPI sem tocar em uma única linha de caso de uso; <strong>comunicação clara entre camadas</strong> via portos e adaptadores mantém o projeto organizado mesmo quando cresce de 10 para 100 mil linhas.</p>
<p>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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/" target="_blank" rel="noopener noreferrer">Clean Architecture: A Craftsman's Guide to Software Structure and Design - Robert C. Martin</a></li>
<li><a href="https://docs.python.org/3/library/abc.html" target="_blank" rel="noopener noreferrer">Documentação Python: abc (Abstract Base Classes)</a></li>
<li><a href="https://alistair.cockburn.us/hexagonal-architecture/" target="_blank" rel="noopener noreferrer">Hexagonal Architecture (Ports & Adapters) - Alistair Cockburn</a></li>
<li><a href="https://www.oreilly.com/library/view/domain-driven-design-tackling/9780321125675/" target="_blank" rel="noopener noreferrer">Domain-Driven Design - Eric Evans</a></li>
<li><a href="https://realpython.com/dependency-injection-python/" target="_blank" rel="noopener noreferrer">Dependency Injection in Python - Real Python</a></li>
</ul>
<p><!-- FIM --></p>