Over Engineering e times reféns da própria arquitetura

Apesar de ser capaz de gerar uma série de impactos nem sempre abordamos este assunto na rotina dos times com a devida atenção.

Matthaus Schall Lopez
10 min readJun 6, 2020

Por que falar sobre over engineering?

Confesso que este tema vem me inquietando a bastante tempo, o que inclusive me levou a participar como palestrante no TDC e Concrete On Beer (ambos em Belo Horizonte) trazendo esta discussão para mesa, além do Kotlin Everywhere em que trouxe uma visão crítica dos potenciais da linguagem.

E o motivo disto? Observar o quanto a busca por arquiteturas, abstrações, "patterns" (e qualquer outa expressão do tipo) perfeitas pode gerar impactos não somente no código produzido mas também na interação entre os integrantes de um time, o potencial de entrega e, por consequência, o produto em curto e longo prazo.

É óbvio que quando iniciamos o desenvolvimentos de um produto do zero ou damos sustentação a algo já existente buscamos empregar as melhores práticas, analisar os requisitos técnicos e de negócio e estudarmos sobre as melhores abordagens para aquele problema / solução. Mas como fazemos isso? A qual custo? Analisamos os impactos além do código?

Mas o que é over engineering?

Vou deixar este trabalho para algumas referências escolhidas em materiais passados!

Overengineering (or over-engineering) is the act of designing a product to be more robust or have more features than necessary for its intended use.

Source: https://en.wikipedia.org/wiki/Overengineering

Source: Unknown (if you know, just send me, please)

Over-Engineering is when someone decides to build more than what is really necessary based on speculation

Source: https://hackernoon.com/how-to-accept-over-engineering-for-what-it-really-is-6fca9a919263

Com estas citações e as polêmicas que elas trazem implicitamente, vamos abordar um pouco mais sobre o assunto dentro da "vida real" — melhor do que entrarmos em discussões sobre definições ;).

O que pode levar a over engineering?

A busca pela solução perfeita para um problema ou a tentativa de antecipar os possíveis cenários futuros são claros exemplos que podem nos levar a cometer exageros na engenharia de software. Mas seriam somente estes os casos?

Reaproveitamento de código

É um fato que o reaproveitamento de código é uma boa prática, mas por quê?

  • Obviamente evitamos rescrever os mesmos códigos e ganhamos em produtividade — (quase sempre) (assunto para outro artigo)
  • Somos "obrigados" a fazer uma exaustão de cenários para poder suportar diferentes situações — o que pode nos ajudar a levantar problemas.
  • Considerando envolver boas abstrações, por que não cobrir com testes? Mais reaproveitamento, menos código, talvez mais testes automatizados.

Mas é somente isto que acontece neste processo?

  • Quando foi a última vez em que tínhamos uma função simples e ao tentar reaproveitar em vários pontos terminamos com a mesma função cheia de condicionais e fugindo da responsabilidade única? (um salve para o SOLID).
  • E aqueles controllers, view models, activities e classes com milhares de linhas de código? Também conhecidas como god classes.

Mas por quê isto ocorre?

  • Muitas vezes buscamos convergir em nosso código (super reaproveitável) várias regras e lógicas de negócio que nem sempre convergem na vida real. Qual foi a última vez que você tentou criar um super componente para exibir um dado (que seria igual em todo o "sistema") e na semana seguinte descobriu que o para um tipo específico de dados, aquele componente teria de exibir diferente? Completamente diferente!!!
  • E aquelas vezes que queremos definir no início do projeto todos as abstrações, regras e entidades? Nem analisamos todos os endpoints que vamos precisar e já estamos criando super componentes e super classes para gerenciar situações em toda a vida do projeto.

Dê tempo ao tempo e construa soluções que suportem serem modificadas no futuro sem causar grandes impactos, mesmo que isso em primeiro momento gere duplicações.

É mais fácil você entender e refatorar código simples e legível do que partir de algo que você não entende — você vai acabar jogando fora e construindo novamente.

Com o tempo os requisitos de negócio e técnicos irão se estabilizar e você terá melhor clareza para saber o que poderá ser "centralizado".

Uma arquitetura bem desenhada é aquela que suporta mudanças e não aquela que já nasce com todas as abstrações, generalizações e decisões tomadas — não adianta um time adaptável e ágil se o seu código não corresponde.

Generics e relacionados

Que o código fica com uma super cara profissional e de complexidade quando usamos "generics" é um fato. Mas isso é tudo?

Source: https://medium.com/omnius/wildcards-in-java-generics-part-1-3-dd2ce5b0e59a

O Java foi uma "vítima" (como exemplo) pela proximidade com a minha principal área de atuação (desenvolvimento Android) mas não é exclusividade da linguagem.

A construção de códigos que utilizam listas, "mappers", "adapters" e para classes utilitárias e afins muitas vezes abusam do uso de generics na busca de manipular diferentes tipos e subtipos de objetos e componentes. Os resultados são sofisticados mas o caminho nem sempre é linear.

  • Por muitas vezes utilizamos mais tempo buscando a abstração perfeita entre tipos (para prover a possiblidade do generic) do que nas operações especializada para cada um dos problemas.
  • Podemos cair por acaso em funções com múltiplas responsabilidade.
  • A legibilidade do código pode ficar "comprometida". Não para aquele que o constrói mas sim para os outros integrantes do time ou que utilizam o mesmo codebase.

Analise com cuidado a necessidade de uso de generics e relativos em outras linguagens — especialmente em soluções mágicas que encontramos prontas pela internet para solucionar problemas. Podemos facilmente cair na inércia de apenas copiar de um projeto para outro e nem mesmo entender o seu funcionamento.

Euforia, entusiasmo e viés

Quase que na sua totalidade, desenvolvedores gostam de estudar novas ferramentas, abordagens e obviamente experimentar estas descobertas. Porém existem momentos e momentos para fazermos isto.

Com hypes e mais hypes, artigos e soluções que dizem serem definitivas para algo, não é raro encontrar usos extensivos de uma determinada abordagem dentro de um ou mais projetos.

  • Você já teve aquela sensação de que só copiaram a estrutura de um projeto em um novo?
  • E aquelas DSLs e extension functions sendo utilizadas para "tudo"?
  • Clean architecture, clean code ou SOLID apenas copiados mas sem o completo entendimento das premissas.

É difícil encontrarmos soluções definitivas e que não possuam prós e contras a serem analisados. Quando falamos de padrões / filosofias arquiteturais vamos muito mais a fundo pois elas podem sofrer mutações de acordo com os requisitos de um projeto.

Por este motivo não devemos encarar estas escolhas de maneira enviesada e muito menos como uma questão pessoal — não é porque a sua solução não é a ideal para o momento que ela passa a ser ruim.

Divida com o seu time não somente os pontos fortes de uma abordagem mas também os negativos — assim teremos um produto final não somente democrático mas também com uma exaustão de cenários, situações e críticas melhor organizado.

Esteja pronto para ouvir!

É possível ser refém da sua própria arquitetura?

Precisamos olhar não somente para os critérios e consequências técnicas mas também para o que pode representar para a vida do time e de um produto a prática de over engineering.

Paralelização e manejo de tarefas

Manejar tarefas dentro de um time e conseguir paralelizar implementações não deveria ser um sacrifício ou um problema — afinal ter mais de um desenvolvedor por área de atuação em um time é algo comum.

Aliás, não é isto que buscamos com bons padrões e filosofias arquiteturais? Desacoplamento, independência e noção clara de impactos no código…

Porém a medida que o nosso código cresce, a complexidade também pode crescer e se exagerarmos (sem necessidade) na engenharia podemos ter:

  • Várias "pessoas" trabalhando exaustivamente em cima dos mesmos arquivos e lógicas. Lembra das abstrações, generalizações e "god classes"?
  • Conflitos complexos de se resolver passando a ser rotina do time. Situações que podem acabar bloqueando todo mundo enquanto a solução no "core" não é resolvida.
  • Entendimento da extensão do impacto de uma mudança prejudicado

Curva de aprendizado

É um consenso que boas arquiteturas devem exatamente existir para evitar que casos como os anteriores aconteçam. Mas e se complicamos estes padrões a ponto de serem difíceis até mesmo de serem entendidos?

  • A dificuldade de adicionar novos integrantes ao time é clara — precisamos parar os mais experientes, que geralmente estão em tarefas críticas, para explicar o básico do funcionamento. Começar a "performar" então, nem tão cedo.
  • Ver seniors arrancando os cabelos para resolver problemas que deveriam ser triviais para o negócio também não é algo mais tão raro. Refatorar por semanas passa a virar realidade.
  • A resiliência do time fica abalada — ter alguém passando mal ou tirando férias passa a ser um problema que vai do SM ao PM em escaladas monumentais.

Engajamento

Não adianta termos um produto bacana ou uma empresa que entregue um ambiente que propicie a criatividade e a interação se não conseguimos ter este mesmo reflexo no dia a dia de cada desenvolvedor dentro do seu time — afinal a maior parte do tempo ele vai passar codificando, passando raiva ou aprendendo.

  • A dificuldade de entender um trecho de código pode ser frustrante, especialmente quando ninguém consegue dar explicações claras do seu funcionamento ou das suas motivações.
  • A famosa sensação de "ownership" se perde. Se você não entende, você não consegue sugerir melhorias e contribuir de maneira satisfatória. Por fim você não está mais colaborando para o projeto, está somente copiando e colando ou se esforçando para parecer produtivo.

Mas o que podemos fazer?

Agora que já discutimos um pouco do que pode acontecer dentro do nosso código, time e produto, vamos discutir algumas ações e cuidados que podemos tomar no nosso dia a dia.

Ressalto apenas que soluções perfeitas para toda e qualquer situação não existem — pense de maneira crítica e encontre algo que se encaixe melhor para o seu cenário!

Entendimento de negócio

Ter a oportunidade de compreender os requisitos de negócio de um projeto é direito de todos os envolvidos no projeto / codificação em questão, e por isso:

  • Na impossibilidade de todos estarem nas reuniões com as áreas de negócio ou diretamente com o cliente (em refinamentos, grooming etc), promova rodízios entre os integrantes — além de estar capacitando o seu time você está promovendo a disseminação de conhecimento.
  • Criar pares entre pessoas mais experientes e novos integrantes é uma ótima iniciativa para nivelamento de experiências e por consequência acabar provendo oportunidade para que todos possam opinar nas possíveis soluções.
  • Conhecendo bem o negócio você terá a oportunidade de criar algo adequado a necessidade (e não baseado em especulações) — com isto você também poderá evitar de criar algo mais complexo do que o necessário.
  • Se atenha aos requisitos de negócio atuais e não pratique "futurologia" — uma boa arquitetura é aquela que permite melhorias futuras e não aquela em que tudo é definido no primeiro dia (com correria).

Comprometimento

Apesar de algumas vezes observarmos um conceito deturpado de comprometimento (como aceitação de metas intangíveis), o que está por trás dessa palavra pode ser uma série de pontos positivos:

  • Considerando que já participamos dos conhecimentos de negócio no item anterior, é o momento ideal de também participarmos dos momentos de decisão arquitetural — você ganhará muito no sentimento de ownership de projeto e de quebra mais impulso no nivelamento técnico.
  • Atingir metas é uma das consequências de um time comprometido e não o contrário. Mas são as metas estimadas pelo time de desenvolvimento, ok? Afinal não precisamos nos preocupar com super estimativas em times comprometidos e com voz ativa.
  • Com todos comprometidos, mais questionamentos irão surgir na concepção das ideias e possivelmente soluções mirabolantes serão facilmente identificadas e descartadas.

Nivelamento técnico

Não perca oportunidades de nivelar o seu time tecnicamente.

  • Use e abuse de POC para permitir o estudo de novas implementações — independente da experiência podemos experimentar sem medo.
  • Promova "talks" dentro dos seus times — esse é um momento sagrado para serem compartilhados conhecimentos em um ambiente seguro e restrito.
  • Permita um ambiente favorável a "XP" e "pair programming". Desenvolvedores trabalhando juntos potencialmente desenharão soluções em conjunto. Soluções feitas em conjunto levantam discussões e discussões aumentam o senso crítico para exageros.
  • Todos integrantes de um time acrescentam para a solução, independente da sua experiência e background — escute a todos.

Conclusões

Poderia encerrar este discussão com palavras filosóficas ou frases de efeito porém irei para algumas opiniões pessoais (além de algumas que fui deixando pelo caminho).

  • Lembre-se sempre do KISS (Keep It Simple Stupid) e do YAGNI (You aren’t gonna need it) — pode ser um mantra :).
  • Pare de construir a arquitetura de um software como se estivesse construindo um prédio onde precisamos finalizar toda a fundação antes de ir para o próximo passo.
  • Seja flexível e postergue decisões para quando você tiver melhor entendimento de negócio — as coisas vão mudar, nós sabemos.
  • Valorize e capacite seu time — tudo começa, evolui e termina aqui.

Agradecimentos

  • Aos colegas de trabalho atuais e passados a quem tenho e tive a honra de trabalhar e viver as angustias deste tema.
  • Em especial ao amigo e também colega de trabalho André Paulovich que tive a oportunidade ímpar de palestrar no TDC BH e compartilhar ideias na trilha de arquitetura.

Nos vemos nos próximos artigos relacionados! Críticas, sugestões e discussões são sempre bem vindas! :)

--

--

Matthaus Schall Lopez

Brazilian Mobile Specialist passionate by new technologies and its applications in the real world.