Sobre overengineering, ou como ser desnecessariamente complexo

Construir sistemas que sejam simples e elegantes é uma tarefa difícil. Uma forma de diminuir a complexidade desnecessária é pensar em qual o objetivo do sistema, e fazer algumas perguntas simples para validar o caminho. Eu gosto de usar o KISS (Keep It Simple, Stupid) quando estou pensando em resolver um problema de software e nesse artigo vou trazer um pouco da minha experiência usando essa abordagem.

Lauri P. Laux Jr

Lauri P. Laux Jr

February 23, 2021 | leitura de 8 minutos

dev

A Wikipedia traz como overengineering (em tradução livre é super engenharia) como sendo o ato de projetar um produto para ser mais robusto ou ter mais recursos do que o necessário para o uso pretendido, ou para que um processo seja desnecessariamente complexo ou ineficiente.

Note a parte que diz ter mais recursos que o necessário para o uso pretendido. Se o nosso objetivo é construir uma calculadora que faça soma, subtração, divisão e multiplicação, adicionar uma operação de raiz quadrada está fora do uso pretendido e adiciona complexidade desnecessária para o produto.

Isso não quer dizer que a calculadora não deva ter uma arquitetura em que seja simples incluir novas operações matemáticas, significa que para o uso pretendido do produto isso não é necessário e se for colocado no backlog irá drenar esforço do time para algo que não nos deixa mais perto da entrega da calculadora funcionando, testada e em produção. Ao contrário, nos afasta dela.

KISS - Keep It Simple, Stupid.

A primeira vez que escutei KISS (o princípio, não a banda) foi em um projeto de automação para um grande banco. Eu trabalhava em uma equipe com pessoas de várias partes do mundo. O líder técnico do projeto, que era espanhol e costumava falar o que viesse à cabeça, um dia virou para mim, depois de uma discussão sobre mensagens entre a ATM e o Host e disse: KISS. Olhei para ele sem entender nada e ele completou: Keep It Simple, Stupid.

Eu, um pouco confuso, pensei: “então é pra fazer de qualquer jeito”?

Não. O que ele queria dizer com aquela frase era algo mais ou menos assim: “não pire, foque em resolver o problema que você tem nas mãos. Isto é, garanta que a comunicação entre a ATM e o Host se dê de forma segura e confiável e que o código para isso seja simples e fácil de manter. Então, “a simplicidade deve ser uma meta principal no projeto e a complexidade desnecessária deve ser evitada”.

Seguir o KISS, pensar na frase “deixe simples” não implica “fazer de qualquer jeito” ou, ainda mais grave, “não se preocupar com a saúde do código ou desempenho do produto”. Outra reação comum ao KISS é: “então, vamos fazer de qualquer jeito já que precisamos manter o produto simples. Não vamos nos preocupar com desempenho ou confiabilidade”. Ou não vamos testar, criar testes unitários, porque essa implementação “simples” está aí só como quebra galho.

Definitivamente não. Construir uma funcionalidade, uma API ou qualquer outra peça de software de maneira simples implica em entender bem o problema para elaborar uma a solução que tenha a menor complexidade possível de maneira elegante e fácil de alterar no futuro. Um desafio e tanto. Simplicidade e elegância, juntos, é algo difícil de alcançar, mas que rende frutos no futuro.

Como identificar overengineering?

Toda vez que a solução for agregar complexidade ao produto ou sistema é interessante fazer algumas perguntas como: Eu realmente preciso dessa lib ou framework? O que eu ganho em troca aumentando a complexidade do desenvolvimento? Essa featurea/lib/whatever vai ser mesmo usada?

Suponha que você está fazendo um sistema de monitoramento do jardim da sua casa ou condomínio, e escolhemos usar um Arduino Uno e um sensor de temperatura e umidade como o DHT11 para isso. O problema que estamos tentando resolver é enviar esses dados (temperatura e umidade) para serem mostrados em um dashboard em tempo real através de um post para uma URL já definida. O objetivo do dashboard é mostrar esses dados em uma linha do tempo de variação de temperatura e umidade durante o dia, e vamos ter apenas alguns arduinos enviando informações para o dashboard.

Existem várias formas de resolver essa situação. Vou sugerir a solução que entendo ser a mais simples e elegante (que provavelmente pode ser ainda mais simplificada) e depois vou acrescentar uma pitada de overengineering para ver como ela fica.

Aplicando o KISS ao problema, a primeira pergunta poderia ser: Qual a forma mais simples de trocar informações entre o arduino e outro dispositivo? Bem, ele tem uma porta serial, e todos os dispositivos nos últimos 40 anos tem comunicação serial de algum tipo. Legal, consigo que ele se comunique com o mundo externo via porta serial.

A segunda pergunta poderia ser: Tem alguma forma simples de se conectar a uma rede Wifi a partir de uma Arduino? Sim, tem, você pode usar algo como um ESP8266 para que o arduino se conecte nele via serial e consiga acessar uma rede wifi e, assim, enviar os dados para uma URL. E os dados para a URL? Bem, o próprio arduino pode conectar e fazer esse post e o servidor web do outro lado vai fazer o trabalho que ele tem que fazer, recebendo os posts da maneira correta em um endpoint, e o endpoint deve salvar os dados em uma base de dados.

E o dashboard? Bem, podemos usar o Metabase que é free, open source, e tem ótimas ferramentas pronta para gráficos.

Mas essa solução pode rapidamente se tornar bastante complexa, basta que o foco na solução do problema seja levemente desfocada e a gente passe a pensar em coisas que seriam legais ou desejáveis no sistema.

A essa lista pode incluir colocar uma bateria ligada ao Arduino para quando faltar energia elétrica (mas a solução não pediu alta disponibilidade). Vamos usar I2C para conectar os Arduinos e trocar informações importantes entre eles (a solução não requer que os Arduinos se conversem) . A quantidade de dispositivos enviando dados simultaneamente pode ser cem vezes maior, então vamos implementar uma fila para receber os dados desses dispositivos e salvar na base de dados na ordem que as mensagens foram recebidas (a solução não pede ordenação de mensagem e não pede suporte a centenas de dispositivos). Temos que usar uma base de dados NoSQL para guardar essas informações de sensores para ter flexibilidade de adicionar novos modelos (a solução fala apenas no DHT11 e, apesar de ser um nice to have ter suporte a eventuais novos sensores, isso não é obrigatório para a solução).

O dashboard nós iremos desenvolver em React porque teremos mais flexibilidade e controle (a solução pede um gráfico simples de tempo, a complexidade da aplicação React para isso pode ser maior que a solução com o Arduino como um todo. Pense no deploy, testes e manutenção desse aplicativo React).

Acho que deu para ter uma ideia de como uma solução pode rapidamente ter um “surto” de overengineering e ficar muitas vezes mais complexa do que realmente necessitamos. Nesse momento, lembre do KISS (não da banda, do acrônimo) e tente se perguntar se o caminho até a solução não está dando mais voltas que o necessário.

De volta aos problemas imaginários.

Já escrevi sobre problemas imaginários. Aqueles problemas que, na realidade, não precisam ser resolvidos porque, bem, eles não levam você mais próximo da solução ou da entrega do seu projeto. Nesse contexto, o KISS é um princípio que funciona muito bem para criticar se as decisões de design, arquitetura ou desenvolvimento estão realmente resolvendo o que é realmente necessário.

Um tema recorrente é performance (eu prefiro o termo desempenho) geralmente levantado para justificar complexidade ou a adição da mais nova framework da moda. Frases como "com a tecnologia x nossa aplicação irá suportar milhares de requisições" ou "vamos evitar requests frequentes ao nosso web server" são comuns. Quando ouvir algo nessa linha lembre-se de reduzir a questão a essência e se questionar se realmente escala é um problema no início de um projeto (spolers: raramente é), se realmente diminuir a quantidade de request é necessário (um web server é construído para suportar milhares de conexões). Muito mais importante é seu projeto ter seu fluxo principal, ou principais funcionalidades, prontas, testadas e confiáveis.

No mundo do desenvolvimento de games é comum o lançamento de um beta de um jogo qualquer onde o desempenho não foi otimizado. Claro, o objetivo do beta é validar se o jogo é divertido, se ele não quebra no meio de uma conquista importante ou se é necessário repensar alguma mecânica. Otimizar o desempenho de um jogo que fundamentalmente está quebrado não leva a nada, não leva o jogo a estar mais próximo do lançamento, é resolver um problema secundário naquele momento. O KISS nesse contexto é fazer o jogo funcionar sem quebrar, ter uma execução estável e confiável e usar para isso as soluções mais simples possíveis. Atingiu o objetivo e o jogo está estável? Agora sim o desempenho pode ser um ponto de atenção e a otimização passa a ser um problema real.

KISS (o princípio) é do bem! Use sempre que possível para ter projetos com menos complexidade do que realmente você precisa.

Lauri P. Laux Jr
Lauri P. Laux Jr

Mestre em Ciência da Computação. Trabalhei muitos anos com Java para big corps, mas agora Python mora no meu coração. No tempo livre narro RPG, gamer de jogos single player e cervejeiro artesanal.

LinkedInInstagram