Pular para o conteúdo
blog.kristyan.dev
arquitetura programação engenharia go opinião

Software genérico: o imposto oculto da abstração

Emaranhado opinativo e informativo sobre software genérico com base nas minhas leituras da semana


Reutilizar código é inteligente. O problema é que a indústria transformou o reuso em um dogma inquestionável, onde escrever duas linhas de código em casa é pecado mortal e importar uma biblioteca genérica de dez mil linhas é virtude de engenharia.

Você precisa de um parser de data? Importa uma biblioteca. Precisa de um validador de struct? Importa uma biblioteca. Precisa de um client HTTP um pouco mais esperto? Importa um framework que promete resolver todos os problemas de rede da história da humanidade.

E aí mora o problema.

Não porque reaproveitar código seja ruim por si só. A biblioteca padrão do Go está aí para provar que uma boa stdlib carrega o piano de quase qualquer aplicação comercial sem pedir ajuda. O problema real é o software genérico por obsessão, aquele que tenta rodar em qualquer ambiente, sob qualquer configuração, atendendo a qualquer caso de uso imaginável do universo.

Na tentativa de agradar a todos, esse tipo de software cria tantas camadas de abstração que se torna pesado, lento e terrivelmente complexo. Ele deixa de ser uma solução elegante para virar um imposto oculto na sua aplicação.

Bonito no GitHub, um inferno no go.mod.

O ecossistema expandido e a profecia de Rob Pike

Como desenvolvedor Go, a frase mais famosa de Rob Pike provavelmente já cruzou o seu terminal ou o seu feed:

“A little copying is better than a little dependency.”

Pike apresentou esse conceito de forma detalhada na palestra clássica da dotGo 2015 (no compilado de Go Proverbs). O argumento dele ataca diretamente a cultura do software hiper-genérico. Quando você traz uma dependência externa gigante para resolver um problema minúsculo, você não está importando apenas uma função; você está importando as decisões arquiteturais, os bugs de concorrência, o grafo de subdependências e as falhas de segurança de outra pessoa.

Antes disso, no ensaio oficial “Less is exponentially more” (2012), Pike já destrinchava a filosofia por trás do design do Go. Ele mostrava como linguagens como C++ e Java inflaram porque tentaram adotar todo tipo de recurso genérico e hierarquia de tipos imaginável. O resultado? Sistemas onde o programador gasta mais tempo gerenciando a complexidade da própria arquitetura do que resolvendo o problema de negócio.

O diagrama abaixo ilustra o que acontece na prática quando trocamos uma linha de código sob medida por uma abstração genérica:

O Custo de uma Dependência Genérica

A engenharia que esqueceu o hardware

Quando o software se torna excessivamente genérico para poder rodar em qualquer lugar, ele se desconecta da máquina física.

No ensaio clássico “A Plea for Lean Software” (1995), publicado na Computer, Niklaus Wirth (o criador do Pascal) já alertava sobre essa tendência de inchaço na seção “Where software heft comes from”. Wirth argumenta que a facilidade de obter hardware mais rápido mascarou a incompetência da indústria em projetar softwares eficientes. Em vez de refinarmos algoritmos para os dados que temos, jogamos camadas de abstrações dinâmicas e genéricas que devoram memória. O software fica gordo. E, como diz a própria Lei de Wirth, ele desacelera mais rápido do que o hardware consegue avançar.

Essa desconexão é o alvo das críticas atuais de programadores de sistemas como Casey Muratori. No seu artigo e benchmark comparativo “Clean Code, Horrible Performance” (2023), Muratori provou o custo real de seguir cegamente regras rígidas de abstração genérica, como as defendidas no movimento tradicional de Clean Code.

Ao criar getters, setters, polimorfismo dinâmico e invocações genéricas para proteger o código contra futuras mudanças teóricas, os desenvolvedores criaram um código que roda até 100 vezes mais lento do que uma função direta que simplesmente processa um array de dados na memória de forma linear.

Módulos profundos vs. Astronautas da Arquitetura

Para não parecer que a solução é reescrever o mundo em Assembly toda segunda-feira, o equilíbrio reside no conceito que John Ousterhout traz no livro “A Philosophy of Software Design” (2018).

No Capítulo 6 (“General-Purpose Modules”), Ousterhout introduz uma discussão cirúrgica sobre até onde um código deve ser genérico. Ele defende que os módulos devem ser profundos (Deep Modules), ou seja, devem ter uma interface simples, mas implementar uma funcionalidade robusta por trás.

No entanto, no mesmo capítulo, ele traça o limite exato de onde a genericidade vira um tiro no pé. Se você começa a injetar dezenas de parâmetros de configuração e comportamentos alternativos para prever cenários hipotéticos que o seu negócio nunca vai enfrentar, você está criando um módulo raso, complexo e inflado. É o que Joel Spolsky chamava de Astronautas da Arquitetura: engenheiros que sobem tanto o nível de abstração que esquecem como o oxigênio funciona aqui embaixo.

O custo de customizar, esticar e entortar uma grande ferramenta genérica para que ela obedeça às regras específicas da sua empresa frequentemente supera, e muito, o custo de ter escrito uma solução interna simples desde o início. É a famosa complexidade acidental que Fred Brooks descreve detalhadamente na seção “Buy versus Build” do seu lendário manifesto “No Silver Bullet” (1986).

O “Suficientemente Bom” sempre vence o “Perfeitamente Abstrato”

Se a história da computação nos ensinou algo, é que a simplicidade da implementação prática sempre vence a perfeição teórica da genericidade absoluta.

Esse fenômeno foi batizado por Richard P. Gabriel no ensaio “The Rise of Worse is Better” (1991). Na Seção 2.1, ele compara a abordagem do MIT (que buscava designs ultra-genéricos, corretude absoluta e arquiteturas perfeitas) com a abordagem de New Jersey (adotada na criação do Unix e do C).

A filosofia de New Jersey dizia que a simplicidade do design é importante, mas a simplicidade da implementação é ainda mais crítica. O software precisa ser fácil de entender, direto ao ponto e focado em resolver o caso comum com eficiência, mesmo que não cubra perfeitamente 100% dos cenários abstratos do mundo acadêmico. O Unix e o C dominaram o planeta exatamente por causa disso.

Menos Slogan, Mais Perfilamento

Antes de instalar aquela biblioteca que promete abstrair o seu banco de dados, a sua fila de mensageria e o seu café da manhã em uma única interface mágica e genérica, faça a si mesmo algumas perguntas:

  • Eu realmente preciso de suporte a 5 bancos de dados diferentes se a minha infraestrutura inteira roda no Postgres há 8 anos?
  • Essa interface genérica não está escondendo o uso ineficiente de memória (como alocações ocultas na heap em Go)?
  • Quanto tempo eu vou gastar lendo a documentação dessa biblioteca comparado ao tempo que eu gastaria escrevendo a lógica exata que eu preciso?

Software genérico tem o seu valor para prototipagem rápida e ecossistemas compartilhados. Mas em produção, no hot path do seu serviço, o código que conhece os seus dados e respeita o seu hardware sempre vai andar de cabeça erguida.

Falta de critério arquitetural compila em qualquer linguagem. Só que o custo da CPU, esse sempre chega na fatura do mês seguinte.