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:

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



Riverfount
Vicente Eduardo Ribeiro Marçal