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.
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
Code that solves problems you don’t have.
Source: https://stackoverflow.com/questions/1941770/concrete-symptoms-of-over-engineering/1941904#1941904
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?
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! :)