A natureza da complexidade de software

Willyan Guimarães
experienceCode
Published in
6 min readDec 27, 2022
Tudo saindo conforme o planejado

Você consegue dizer quando um projeto em que está trabalhando é difícil de compreender, dominar e trabalhar com segurança ? O que de fato caracteriza e significa dizer que um software é complexo ?

É preciso ressaltar que estamos aqui falamos apenas de design de código, nada mais. Esqueça mesmo que sejam relevantes linguagem, frameworks e ferramentas adicionais.

Se pudéssemos classificar a importância de habilidades para um(a) dev(a), essa skill seria bastante relevante.

Mas na prática, não é tão simples assim. Escrever um design simples é muito mais trabalhoso do que dizer que algo é ou não complexo.

Vamos fazer um combinado: Design de Software é um assunto super complicado e até certa medida polêmcio. Mas vamos nos ater ao tema “Complexidade”.

O que é Complexidade?

Complexidade é quando uma estrutura de software, seu codebase, é difícil de entender e modificar. Ela pode se apresentar de várias formas, exemplos:

  • É difícil de entender um pedaço de código e como ele funciona
  • É super trabalhoso e exige tempo considerável para uma pequena mudança
  • Temos uma nova feature a fazer, é obscuro sabermos onde é necessário ajustar
  • Corrigimos um bug, introduzimos vários

Em termos de esforço, um sistema complexo exige-se muito para pequenas mudanças. Sistemas simples, mudanças expressivas são feitas com muito menos esforço.

Complexidade não é sobre quantidade de linhas de código. Vamos desconsiderar essa ordem de grandeza.

Complexidade não pode ser encarado como visão pessoal, nossa missão como desenvolvedor(a) é criar um código simples não para si, mas para os outros.

Sintomas da complexidade

A complexidade se manifesta de três formas, descritas abaixo.

Propagação de mudança

Observe este cenário:

Pense o seguinte, esses são arquivos de layout de um site. A empresa mudou a identidade visual e precisa alterar sua cor de azul para verde. Um dev iniciante no time pegou a modificação para fazer e esqueceu um dos arquivos de layout. Dessa forma, quando uma das páginas forem carregadas vamos ver um comportamento inesperado.

Esse cenário caracteriza-se por Propagação de mudança pois temos vários pontos de ajuste espalhados pelo código. Isso dificulta bastante a rastreabilidade e exige muitas vezes um conhecimento profundo para mitigar o risco.

Carga cognitiva

Sabe quando você pega uma nova feature para desenvolver e precisa entender bastante do assunto para começar ?
Sabe quando você termina essa mesma feature e mesmo com ela já testada se pergunta: “Será que eu não esqueci de nada ?”

Apesar de contextuais esses cenários, eles não são incomuns. Muitos sistemas escondem situações não muito claras em suas funções, APIs, bibliotecas. Quer um exemplo mais pragmático (apesar de bem simples) ?

Precisamos mudar uma chamada feita em nosso código para uma nova versão de um componente.

Na chamada existente hoje, temos:

Precisamos então ajustar:

Ok! Tudo certo.

Esse sistema vai pra produção, e de repente começa a rolar um NullPointerException.

Uma das mudanças, além de novos atributos no retorno é que o método passou a retornar uma referência nula, cenário que não ocorria na versão anterior.

Para além de discussões sobre contrato, exceções, retornos temos aqui uma clara situação de carga cognitiva. Você como dev(a) precisaria saber que era necessária uma validação para conferir se o objeto não é nulo. Esse tipo de situação impõe uma “sabedoria velada” do funcionamento das coisas que utilizamos.

Situações análogas e até mais confusas do essa são muito comuns. Este é um exemplo de carga cognitiva.

Incógnita

Pior que a carga cognitiva e a propagação de mudanças temos o que podemos chamar “Incógnita”, ou até o “Desconhecido”.

Veja esse cenário:

Uma equipe de devs cuida de uma aplicação de missão crítica para um banco. Uma das funções desse sistema, é processar e enviar todo o movimento de pagamentos diário para os sistemas de contabilidade da instituição financeira. Uma das novas features recentes inclui um novo tipo de pagamento a ser feito pelo sistema. Diferentemente de todos os pagamentos, esse em específico não deve ser incluído para o envio contábil. Apesar de ser relevante, essa informação não foi divulgada entre pessoas dos times envolvidos. Como resultado quando a nova feature foi a produção o problema na área de contabilidade se iniciou nos fechamentos com o processamento indevido dos novos pagamentos.

O He-man já tá esperando pra contar a lição de hoje dessa!

Fica tranquilo !

Uma incógnita significa que há algo que você precisa saber, mas não há uma maneira de você descobrir o que é, ou mesmo se existe um problema de verdade. Talvez alguém saiba, ou também não. 🤡🤡🤡

Existem muitos motivos para surgirem incógnitas, mas merece um capítulo a parte.

O que causa complexidade ?

Agora que existe um entendimento sobre o que é complexidade e seus sintomas podemos falar sobre suas causas: dependências e obscuridade.

Dependência é parte fundamental de um software. É impossível escrever sistemas sem precisar de componentes, frameworks, APIs. Escrever código é criar pequenas partes que relacionam e dependem entre si. Entretanto é preciso buscar conectar seu código com dependências estáveis, confiáveis e o mais óbvias possíveis.

Vamos lembrar do exemplo dos arquivos que apontam a cor de layout de páginas. Foi ajustado para que uma API seja consumida para buscar as cores, ficando agora assim:

Nova versão das páginas busca em uma API a cor a ser utilizada

Dessa forma, uma eventual modificação não nos levaria a alterar vários arquivos, apenas o retorno da API. Assim resolvemos o problema que tínhamos de propagação de mudança alterando a dependência que existe no código, antes espalhada por vários arquivos e agora centralizada em um serviço.

Nesse caso, diminuímos as dependências, de N arquivos para um única fonte. Essa nova API é uma dependência mais clara e óbvia do que a anterior, melhorou ?

A segunda causa é obscuridade, que é o cenário onde uma informação importante não é conhecida ou suficientemente compartilhada e disponível, alguns exemplos:

  • A situação relacionada ao sistema de pagamentos relatada acima
  • Nomes de variáveis genéricos que dificultam entendimento pela sua amplitude de significados
  • Documentação desatualizada ou mesmo com informações relevantes esquecidas
  • Variáveis com nomes iguais mas com significados diferentes em várias partes do código
  • Documentação inadequada

Logo, temos causa efeito:

Sendo que, geralmente:

Acredite, acontece todos os dias

Assim como um projeto atrasa 1 ano um dia de cada vez, um sistema se torna complexo ao ponto de ser difícil entender e manter da mesma forma. Um único pequeno item é inofensivo, mas ao longo do tempo, vai se tornar um grande ofensor.

Concluindo

É importante compreender fatos e as origens pelas quais cenários como esses acima que levam ao aumento de complexidade de software para pensar em formas de mitigar os riscos dos quais ao longo do tempo podem se tornar situações irreversíveis.

“Se você já vivenciou pelo menos um pouco dessa situação, então sabe que dedicar tempo para limpar seu código não é apenas eficaz em termos de custo, mas uma questão de sobrevivência”.

Robert C. Martin

Este texto foi inspirado no primeiro capítulo do livro “A Philosophy of Software Design

--

--