Primitive Obsession no Python: Refatorando com Dataclasses para Value Objects Robustos
Baseado na Live de Python #150 do canal Eduardo Mendes no YouTube, este artigo explora de maneira prática e direta a “primitive obsession” — code smell onde tipos primitivos (strings, dicts, lists) substituem abstrações de domínio ricas — e como dataclasses (Python 3.7+, PEP 557) oferecem solução definitiva para criar Value Objects tipados, imutáveis e comportamentalmente ricos. Para sêniores buscando elevar modelagem DDD e reduzir technical debt em escala.
Primitive Obsession: Raiz do Problema
Primitive obsession ocorre quando entidades de domínio são reduzidas a “bags of primitives”, i. e., em que objetos de domínio são representados apenas como coleções simples de tipos primitivos (como strings, números, listas e dicionários) sem encapsulamento ou comportamento. Ou seja, em vez de ter classes ou estruturas que representem conceitos ricos com regras, validações e métodos, o código manipula “sacos” ou “pacotes” de dados primitivos soltos, o que aumenta a complexidade, propensão a erros e dispersa a lógica de negócio. Aplicando princípios de POO fundamentais: encapsulamento, polimorfismo e responsabilidade única. Consequências incluem:
- Lógica de domínio espalhada: Funções utilitárias fazem parsing manual de strings/dicts
- Falta de invariants: Sem validação, DDDs inválidos ou CPFs malformados passam despercebidos
- Sem comportamentos:
nome_completo()vira função externa poluente - Type unsafety:
KeyErrorem runtime, mypy cego para estruturas aninhadas - Testabilidade pobre: Mockar dicts vs mockar objetos com dependências claras
# ANTES: Primitive Obsession Clássica
dados = [
{"nome": "João", "sobrenome": "Silva", "telefone": {"residencial": "1234-5678"}, "ddd": 11},
{"nome": "Maria", "sobrenome": "Santos", "telefone": {"residencial": "9876-5432"}, "ddd": 99} # DDD inválido!
]
# Lógica espalhada + runtime errors
def nome_completo(dado: dict) -> str:
return f"{dado['nome']} {dado['sobrenome']}"
def telefone_residencial(dado: dict) -> str:
return dado['telefone']['residencial'] # KeyError se faltar!
nomes = [nome_completo(d) for d in dados]
telefone = telefone_residencial(dados[0]) # Fragile!
Custo real: Cyclomatic complexity explode, coverage cai, onboarding demora 3x mais.[9][10][1]
Dataclasses: Antídoto Completo à Primitive Obsession
Dataclasses não são “namedtuples mutáveis”. São geradores de Value Objects que restauram domínio perdido via automação OO inteligente:
1. Encapsulamento Automático + Comportamento
from dataclasses import dataclass, field
from typing import ClassVar
from datetime import date
@dataclass(frozen=True)
class Telefone:
residencial: str
def formatado(self, ddd: int) -> str:
return f"({ddd}) {self.residencial}"
@dataclass(frozen=True)
class Pessoa:
nome: str
sobrenome: str
telefone: Telefone
ddd: int
data_nascimento: date
# Invariant via post_init
def __post_init__(self):
if self.ddd < 11 or self.ddd > 99:
raise ValueError(f"DDD inválido: {self.ddd}")
if self.data_nascimento > date.today():
raise ValueError("Data futura inválida")
@property
def nome_completo(self) -> str:
return f"{self.nome} {self.sobrenome}"
@property
def telefone_formatado(self) -> str:
return self.telefone.formatado(self.ddd)
@property
def idade(self) -> int:
return (date.today() - self.data_nascimento).days // 365
# Value Object equality por valor, não referência
def __eq__(self, other):
if not isinstance(other, Pessoa):
return NotImplemented
return (self.nome_completo, self.ddd) == (other.nome_completo, other.ddd)
2. Uso: Domínio Restaurado, Zero Boilerplate
# Value Objects puros, type-safe
pessoas = [
Pessoa("João", "Silva", Telefone("1234-5678"), 11, date(1990, 5, 15)),
Pessoa("Maria", "Santos", Telefone("9876-5432"), 21, date(1985, 8, 22))
]
# Comportamento encapsulado, sem loops manuais
print([p.nome_completo for p in pessoas])
# ['João Silva', 'Maria Santos']
print(pessoas[0].telefone_formatado) # "(11) 1234-5678"
print(pessoas[0].idade) # ~35
# Validação em tempo de construção
try:
Pessoa("Inválido", "X", Telefone("0000"), 5, date(2026, 1, 1))
except ValueError as e:
print(e) # "DDD inválido: 5"
3. Integrações Avançadas
from dataclasses import asdict, astuple
import json
from typing import Any
# Serialização controlada (não expõe internals)
def pessoa_to_json(p: Pessoa) -> str:
return json.dumps({
"nome_completo": p.nome_completo,
"telefone": p.telefone_formatado,
"idade": p.idade
})
# mypy + Pydantic validation
# pip install pydantic[email-validator]
from pydantic import BaseModel, validator
class PessoaPydantic(BaseModel):
nome_completo: str
telefone: str
idade: int
@validator('telefone')
def validate_telefone(cls, v):
if not v.startswith('('):
raise ValueError('Formato inválido')
return v
Benefícios Arquiteturais Profundos
| Aspecto | Primitive Obsession | Dataclasses Value Objects |
|---|---|---|
| Encapsulamento | 0% (dicts públicos) | 100% (métodos + invariants) |
| Type Safety | Runtime KeyError |
Compile-time + runtime |
| Testabilidade | Mock dicts complexos | Mock objetos com dependências claras |
| Performance | ~1x (dict access) | ~1.2x (dataclass overhead mínimo) |
| Serialização | json.dumps(dict) |
asdict() + versionamento |
| Extensibilidade | Refatorar todos consumers[¹] | Herança + composição |
[1] Dificuldade que ocorre quando a estrutura de dados primitivos é modificada ou evoluída.
Métricas reais: Times reportam 40% menos bugs de dados, 25% mais velocidade em reviews.
Primitive Obsession em Escala: O Verdadeiro Custo
# Microsserviço com 50+ endpoints primitivos
@app.get("/clientes")
def listar_clientes():
return [
{"id": 1, "nome": "João", "email": "joao@email.com", "ativo": True},
# 100+ campos espalhados, validação em middlewares...
]
Virada com dataclasses:
@dataclass(frozen=True)
class Cliente:
id: int
nome_completo: str
email: EmailStr # pydantic
status: ClienteStatus # Enum
@classmethod
def from_db(cls, row: Row) -> "Cliente":
return cls(
id=row.id,
nome_completo=...,
email=row.email,
status=ClienteStatus.from_str(row.status)
)
Conclusão
Dataclasses eliminam primitive obsession restaurando domínio rico sem sacrificar performance ou a experiência do desenvolvedor. São o “sweet spot” entre namedtuples (imutáveis, sem comportamento) e classes manuais (boilerplate pesado).
Vicente Eduardo Ribeiro Marçal