← Voltar aos artigos

Esqueça objetos: comece a pensar em transformações de dados

Pare de modelar conversas entre objetos, comece a compor transformações.

Esqueça objetos: comece a pensar em transformações de dados

Pare de modelar conversas entre objetos, comece a compor transformações.

Introdução

Qualquer pessoa que já tenha trabalhado em uma base de código minimamente complexa conhece essa sensação: você abre um arquivo para mudar algo simples e, de repente, está navegando por uma rede de objetos, serviços, mutações de estado e dependências indiretas só para entender o que realmente está acontecendo.

Os dados ficam escondidos, o comportamento é fragmentado e os efeitos colaterais vazam por todos os lados.

Na maioria das vezes, o problema não é a falta de padrões ou abstrações — é o excesso deles. Costumamos modelar sistemas como se fossem conversas entre objetos, quando, na prática, o que estamos fazendo é muito mais simples: recebemos dados, transformamos esses dados passo a passo e produzimos um resultado.

Se você remover a sintaxe, os frameworks e os buzzwords, a maioria dos programas não passa de uma sequência de transformações. Essa ideia tem um nome — arquitetura em pipeline — mas, mais importante do que o nome, ela representa uma forma diferente de pensar sobre código.

Uma mentalidade de pipeline

Existe uma história famosa do blog "More Shell, Less Egg" que captura perfeitamente essa mentalidade.

Donald Knuth foi convidado a resolver um problema aparentemente simples: ler um arquivo de texto, identificar as n palavras mais frequentes e imprimi-las ordenadas por frequência.

A solução de Knuth ocupou mais de dez páginas escritas em Pascal.

Doug McIlroy, um dos pioneiros do Unix, respondeu com isto:

sh
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed ${1}q

Em vez de um grande algoritmo feito sob medida, McIlroy encadeou pequenas ferramentas independentes. Cada uma faz exatamente uma coisa, e a saída de uma se torna a entrada da próxima.

Sem estado oculto, sem fluxo de controle complexo — apenas dados fluindo adiante. Essa é a essência do pensamento em pipeline.

De conversas entre objetos para transformações de dados

A arquitetura de pipeline é frequentemente explicada como um padrão, mas funciona melhor quando entendida como um modelo mental.

Em sistemas orientados a objetos, os dados geralmente ficam encapsulados dentro de objetos. Os métodos existem principalmente para proteger e mutar o estado interno. À medida que o sistema cresce, isso tende a gerar acoplamento forte e comportamento implícito — você precisa entender quem é dono de quê antes de conseguir alterar qualquer coisa com segurança.

O pensamento em pipeline inverte essa lógica: em vez de acumular estado, você faz os dados fluírem adiante. Os dados passam a ser o elemento central, enquanto as funções se tornam apenas transformações aplicadas ao longo do caminho — cada etapa recebe um valor, transforma e o entrega para a próxima.

Essa ideia não é nova, mas continua extremamente atual. Linguagens como Elixir, F# e Swift adotaram esse modelo há anos por meio de um operador de pipeline (|>). A chegada do PHP 8.5 a esse grupo não é apenas uma mudança sintática — é um convite para repensar como estruturamos o código PHP do dia a dia.

Pipes e Filtros: pontos fortes, limites e realidade

No nível arquitetural, essa ideia se manifesta no padrão pipes and filters.

Um pipeline é composto tipicamente por:

  • Produtores – que iniciam o fluxo
  • Transformadores – que modificam ou enriquecem os dados
  • Testadores – que validam, filtram ou roteiam
  • Consumidores – que terminam o processo

Esta estrutura tem pontos fortes claros e bem conhecidos:

  • Extremamente fácil de entender, em grande parte porque o modelo de execução é linear e explícito
  • Cada estágio é pequeno, focado e substituível, o que resulta em forte modularidade e baixa carga cognitiva
  • Do ponto de vista de custo, isso geralmente se traduz em manutenção mais simples e baixo custo de mudança

Mas também existem trade-offs:

  • Pipelines costumam ser implantados como uma única unidade, especialmente em sistemas monolíticos
  • Se uma etapa falha de forma grave (por exemplo, um erro de out of memory), toda a aplicação é afetada — o que limita tolerância a falhas e elasticidade, além de poder aumentar significativamente o tempo de recuperação em aplicações maiores

Por isso, pipelines não são uma bala de prata para todos os problemas arquiteturais. Seus pontos fortes aparecem em fluxos locais, processamento de requisições, transformação de dados e fluxos de domínio onde clareza, previsibilidade e facilidade de raciocínio são mais importantes do que escalabilidade horizontal ou isolamento fino de falhas.

Quando usados no contexto certo, arquiteturas em pipeline continuam sendo uma das formas mais claras e eficazes de estruturar o fluxo de execução.

Tratamento de erros em um mundo linear

Esta abordagem funciona bem para cenários simples, mas aplicações do mundo real rapidamente levantam uma questão importante: como lidar com erros em um pipeline linear sem sacrificar a legibilidade?

À medida que os sistemas crescem, o tratamento de erros tende a vazar para todas as camadas e a corroer o modelo mental de transformações passo a passo.

O insight central é simples: pipelines permanecem limpos quando os valores nunca são passados em sua forma “crua”. Cada etapa deve operar sobre um wrapper que codifica explicitamente sucesso ou falha, permitindo que erros percorram o pipeline como dados comuns.

Representações comuns incluem Result<T, E> ou Either<E, T> em linguagens tipadas. Elixir expressa o mesmo conceito através de uma convenção em vez de um sistema de tipos estrito, usando tuplas como {:ok, value} e {:error, reason}. Independentemente da sintaxe, o objetivo é o mesmo: tornar a falha um valor de primeira classe, capaz de fluir com segurança pelo sistema.

Existem duas estratégias principais para trabalhar com esses valores encapsulados em um pipeline:

Tratar erros dentro de cada transformação

  • A abordagem mais direta é tratar erros dentro de cada transformação.
  • As funções são escritas com múltiplos caminhos — um que faz pattern-matching sobre um valor de sucesso (por exemplo {:ok, ...} ou Right) e aplica a transformação, e outro que corresponde a um estado de falha (por exemplo {:error, ...} ou Left) e simplesmente o retorna sem alterações.
  • Uma vez que uma falha ocorre, ela se propaga pelo pipeline e impede novas computações. Essa abordagem é direta, mas tende a espalhar lógica de tratamento de erros por muitas funções, aumentando o boilerplate e obscurecendo a lógica central de negócio.

Centralizar o tratamento de erros no pipeline (abordagem preferida)

  • Em vez de tratar erros dentro de cada transformação, é possível extrair a lógica de propagação e deixar que o próprio pipeline orquestre o fluxo de controle por meio de uma operação do tipo bind (também conhecida como and_then, flatMap ou chain).
  • O bind recebe um valor encapsulado e uma função de transformação: se o valor representa sucesso, ele é desempacotado e a função é aplicada; se representa uma falha, a execução é interrompida imediatamente (short-circuit) e o erro é propagado — a função sequer é executada.
  • Em termos de Result ou Either, isso significa compor computações em vez de condicionais. Com essa abordagem, as funções de transformação permanecem puras e altamente focadas: operam sobre dados “crus”, retornam um Result ou Either, e não precisam ter qualquer conhecimento sobre como erros são propagados ou interrompem o fluxo.
  • Com o tratamento centralizado, o pipeline passa a ser uma descrição clara do happy path. Falhas se tornam apenas mais um tipo de dado que interrompe o fluxo automaticamente, resultando em um código que se lê de forma linear, escala melhor com a complexidade e lida com erros de maneira controlada e previsível.

Onde o pensamento em pipeline se encaixa naturalmente

O pensamento em pipeline não está preso a uma linguagem ou paradigma específico.

Ele surge de forma natural sempre que um problema pode ser descrito como uma sequência de transformações, na qual cada etapa recebe uma entrada, produz uma saída e pode falhar de maneira controlada.

Esse modelo aparece em domínios muito diferentes entre si. Workflows de ETL são um exemplo clássico: os dados são extraídos, validados, transformados, enriquecidos e, por fim, carregados. Cada etapa depende da anterior, e falhas precisam interromper o processo (short-circuit) preservando o contexto do que deu errado. A mesma estrutura se aplica a pipelines de processamento de dados, stream processing e batch jobs, onde valores fluem por estágios bem definidos.

O padrão é igualmente comum em fluxos no nível da aplicação. Tratamento de requisições e comandos, use cases de domínio e processos de negócio frequentemente seguem uma narrativa linear: interpretar a entrada, validar regras, aplicar transformações, persistir estado e disparar efeitos colaterais.

Integrações com sistemas externos — como chamadas a APIs de terceiros, normalização de respostas e mapeamento para modelos internos — também se encaixam naturalmente nesse formato.

Em qualquer lugar onde a lógica possa ser resumida como:

primeiro faça isso, depois valide, depois transforme, depois persista

…você já tem um pipeline.

O pensamento em pipeline não introduz exatamente uma nova estrutura, mas revela uma que já estava lá, tornando o fluxo explícito, componível e mais fácil de entender.

Um exemplo prático usando o operador pipe do PHP 8.5

Vamos modelar um workflow comum: criar um usuário a partir de uma requisição recebida. O objetivo aqui não é construir um framework, mas mostrar como o pensamento em pipeline pode emergir com muito pouca estrutura.

Passo 1: um wrapper Result simples

Em vez de introduzir uma hierarquia completa de classes, vamos adotar uma convenção simples e explícita.

php
function ok($value): array
{
    return ['ok' => true, 'value' => $value];
}

function error(string $reason): array
{
    return ['ok' => false, 'error' => $reason];
}

Passo 2: um único primitivo de composição

Todo o pipeline se apoia em uma única operação: aplicar o próximo passo apenas se o anterior tiver sido bem-sucedido.

Este é o conceito bind, flatMap, and_then em sua forma mais simples possível.

php
function then(array $result, callable $fn): array
{
    return $result['ok']
        ? $fn($result['value'])
        : $result;
}

Passo 3: funções de transformação puras

Cada função faz exatamente uma coisa, recebe entrada bruta e retorna um Result. Nenhuma delas sabe que faz parte de um pipeline.

Cada função é fácil de testar e não tem conhecimento algum sobre a orquestração do fluxo.

php
function sanitize(array $input): array
{
    return ok([
        'email' => trim($input['email'] ?? ''),
        'name'  => trim($input['name'] ?? ''),
    ]);
}

function validate(array $data): array
{
    return filter_var($data['email'], FILTER_VALIDATE_EMAIL)
        ? ok($data)
        : error('Invalid email');
}

function createUser(array $data): array
{
    return ok(User::create($data));
}

function sendWelcomeEmail(User $user): array
{
    Mail::send($user->email);

    return ok($user);
}

function present(User $user): array
{
    return ok([
        'id'    => $user->id,
        'email' => $user->email,
    ]);
}

Passo 4: o pipeline em si

O resultado é um fluxo linear e visível, com uma separação clara entre transformação e orquestração, tratamento de erros embutido sem condicionais aninhados e um código que se lê como um processo — não como um quebra-cabeça.

php
$result =
    sanitize($input)
    |> fn($r) => then($r, 'validate')
    |> fn($r) => then($r, 'createUser')
    |> fn($r) => then($r, 'sendWelcomeEmail')
    |> fn($r) => then($r, 'present');

Mesmo sem operadores pipe nativos, este estilo pode ser aplicado hoje usando composição de funções ou helpers simples.

Conclusão: redescobrindo o fluxo

Dos pipelines do Unix shell nos anos 1970 ao operador pipe do PHP 8.5, essa ideia sobreviveu porque funciona.

Pensar em pipelines incentiva você a:

  • Não esconder dados
  • Reduzir estado implícito
  • Favorecer composição em vez de fluxo de controle
  • Escrever código que se explica sozinho

Não se trata de abandonar objetos por completo — mas de saber quando eles não são a melhor ferramenta.

Às vezes, o design mais limpo não é uma hierarquia inteligente de classes, e sim um fluxo simples e explícito de dados.

Então, da próxima vez que um trecho de código PHP parecer mais complexo do que deveria, tente fazer uma pergunta diferente:

Como isso ficaria se fosse apenas um pipeline?

Referências

  1. Doug McIlroy - "More Shell, Less Egg"
  2. David Thomas & Andrew Hunt - "The Pragmatic Programmer"
  3. Mark Richards & Neal Ford - "Fundamentals of Software Architecture"
  4. PHP RFC - "Pipe Operator"
  5. Scott Wlaschin - "Railway Oriented Programming"