Sobre design de software e complexidade

Elton Minetto
Inside PicPay
Published in
6 min readSep 12, 2022

No mês de agosto de 2022, tive a oportunidade de apresentar uma palestra no TDC Business, em São Paulo. O título da palestra foi "Reflexões sobre Design de Software", e neste post quero trazer alguns dos principais pontos apresentados.

A palestra é fortemente inspirada no ótimo livro "A Philosophy of Software Design", do professor John K. Ousterhout. Depois de apresentar a disciplina de "Software Design" na universidade de Stanford por alguns anos, ele reuniu os aprendizados neste livro, que eu recomendo a leitura.

Um dos pontos principais do livro, e que eu quero destacar neste post, é sobre a complexidade. Segundo o autor:

“Complexidade é qualquer coisa relacionada à estrutura de um sistema de software que torna difícil entendê-lo e modificá-lo”

É visível como a tecnologia abraçou todos os mercados e com isso estamos resolvendo problemas cada vez mais complexos, envolvendo inteligência artificial, carros autônomos, criptomoedas, etc. Mas além da complexidade inerente dos negócios nós desenvolvedores acabamos criando outras camadas de complexidade nos projetos.

A complexidade normalmente se manifesta em um projeto de software de três maneiras principais, ordenadas em termos de gravidade crescente:

  • Amplificação de mudança (Change amplification): Quando uma simples mudança requer a modificação do código em muitos lugares;
  • Carga cognitiva (Cognitive load): Quando desenvolvedores precisam carregar muitas informações em suas cabeças para concluir uma tarefa. Isso aumenta as chances de que possam perder alguma coisa, levando a bugs;
  • Desconhecidos desconhecidos (Unknown unknowns): Quando não é óbvio quais informações ou mudanças são necessárias para realizar uma tarefa.

Tenho certeza de que toda pessoa desenvolvedora já enfrentou, ou enfrentará, algum desses cenários em sua carreira.

Outro ponto a ser considerado, é que a complexidade nunca é algo que acontece repentinamente. Geralmente não é uma coisa que torna um sistema complicado, mas um acúmulo de más decisões ao longo de um período de tempo.

Podemos destacar duas razões principais pelas quais os projetos de software se tornam complexos:

  • Dependências — Uma dependência entre duas ou mais partes de um sistema só é desejável se a dependência for clara e óbvia. Quando não está claro qual código depende de outro, mesmo mudanças simples no sistema levarão muito tempo e existe um alto risco de bugs aparecerem em produção.
  • Obscuridade — Isso ocorre quando algumas informações importantes sobre o sistema não são óbvias. Por exemplo, quando não está claro em que ordem executar um conjunto de métodos para realizar uma operação. Se o código não for óbvio, o leitor deve gastar muito tempo e energia para tentar entendê-lo, e a probabilidade de mal-entendidos é alta.

E como reduzir a complexidade?

O autor aponta duas soluções para minimizarmos a complexidade dos nossos projetos. São elas: "Cultivar um bom mindset" e "Escrever bons módulos".

Cultivar um bom mindset

Podemos dividir o processo de desenvolvimento de software em duas mentalidades:

  • Programação tática. Com essa mentalidade o objetivo é colocar a feature ou correção em produção o mais rápido possível. Para isso os times pegam atalhos, não escrevem testes, não refatoram código para melhorar a manutenibilidade e dificilmente pensam em design de software. O resultado é o aumento da complexidade e bugs, insatisfação de desenvolvedores e clientes.
  • Programação estratégica. Com essa mentalidade, o código funcionando não é o suficiente. O objetivo passa a ser produzir um bom design de software, minimizar a complexidade e evitar débitos técnicos. Isso pode tornar o processo de desenvolvimento mais lento no curto prazo, mas ganha-se velocidade com o tempo, e evita-se grandes refatorações futuras.

Como decidir o quanto investir? Como comentado acima, a programação estratégica tem um custo maior de tempo a curto prazo, mas ganha-se no futuro. Se o cenário for de uma startup ou time que precisa fazer um MVP (Minimum Viable Product, em português, Produto Minimamente Viável), talvez seja mais recomendado usar-se a programação tática pois ainda não se tem a certeza do futuro. Mas conforme essa visão de futuro vai se solidificando é necessário aplicar cada vez mais a programação estratégica.

Outras dicas:

  • Faça continuamente pequenos investimentos;
  • Quando escrevendo novos códigos: faça o design com cuidado, invista em documentação;
  • Quando alterando código existente: tente sempre encontrar algo a melhorar. É a famosa "regra do escoteiro".

Escrever bons módulos

A segunda forma de mitigar a complexidade dos projetos é escrever bons módulos. Mas o que significa "módulo" neste contexto? O conceito que vamos usar aqui é:

Um módulo é: “uma unidade de código relativamente independente com uma
interface e uma implementação”. Pode assumir muitas formas, como uma função, classe, pacote ou serviço.

E "interface" neste contexto é:

Interface é tudo que a pessoa precisa para interagir com o código. Não só a assinatura dos métodos, mas também seus efeitos colaterais. É o custo de complexidade que este código impõe ao resto do sistema, por isso deveria ser o menor possível.

Agora que entendemos o que é um módulo, como criar um bom exemplar? O autor do livro dá algumas dicas:

  • Forneça bons “defaults”. Ao escrever um módulo pense na maioria dos seus usuários e como eles vão usar o seu código. Por exemplo, no código a seguir, consideramos que na grande maioria dos cenários o parâmetro default de timeout é o ideal, por isso escondemos essa informação no construtor. E o código dá a opção para quem precisar definir um valor diferente poder fazer isso de maneira simples:
  • Esconda informações que não são importantes. Este item foi exemplificado no tópico anterior, pois foi possível esconder a informação do timeout para a grande maioria dos usuários, tornando o código menos complexo.
  • Elimine erros desnecessários. O autor dá o exemplo de uma função que retorna pedaços de uma string. Caso o usuário do código passar como parâmetros valores maiores do que a string sendo usada, a função poderia simplesmente retornar uma lista de caracteres contendo a parte que pode ser acessada ao invés de um erro. Isso diminui a necessidade do consumidor da função saber os detalhes de tratamento de erros e tem a acesso a mesma funcionalidade.
  • Escreva boa documentação. Sua documentação deveria falar sobre as decisões e cenários que o código resolve e não os detalhes de como isso é feito.
  • Escolha bons nomes. Para funções, variáveis, pacotes, etc.
  • Dê preferência a módulos profundos ao invés de rasos

Módulos rasos? Módulos profundos?? Este foi um dos conceitos que mais me chamou a atenção no livro, por isso vou dedicar um pouco mais de tempo explicando ele:

  • Módulos profundos (deep modules) são aqueles que fornecem interfaces simples para funcionalidades complexas,
  • Módulos rasos (shallow modules) são aqueles que possuem uma interface complicada sem esconder muita complexidade.

O desenho a seguir tenta ilustrar o conceito:

Vamos ver um exemplo de módulo raso:

Segundo o conceito do autor, esse módulo é raso pois ele não esconde detalhes de implementação, o usuário precisa conhecer muita informação (precisa criar um FileInputStream, usá-lo como parâmetro para um BufferedInputStream, criar um terceiro objeto, etc) para ter acesso aos benefícios providos (ler os dados de um arquivo). Além disso, faz sentido pensar que na maioria dos cenários é preferível ler os dados usando um buffer , pois isso aumenta a performance. Nesse caso, isso deveria ser um comportamento default do módulo.

Por outro lado, vamos analisar este módulo:

Essa é a assinatura das funções usadas para ler e gravar arquivos do Unix, definido na década de 70 e usado até hoje por sistemas operacionais como Linux, FreeBSD e macOS. Com estas cinco funções podemos realizar todas as operações em arquivos no sistema operacional. Por trás destas funções temos uma série de detalhes ocultos como: localização no disco, alocação de blocos; gerenciamento de diretórios, procura pelo path; gerenciamento de permissões, etc. Esse é um ótimo exemplo de módulo profundo, pois possui um custo pequeno quando comparado a quantia de benefícios fornecidos.

Conclusão

Com este post eu espero ter despertado o seu interesse em repensar os códigos para minimizar o aumento da complexidade dos projetos, facilitando a sua evolução e tornando o dia a dia da sua equipe mais simples.

Apesar de ainda não existir uma versão em português desde livro, eu recomendo sua leitura. Nos últimos anos a comunidade de desenvolvimento adotou conselhos de um número pequeno de autores como Uncle Bob, Martin Fowler. Apesar da sua inegável contribuição ao assunto é muito bom termos outros autores para apresentarem novas visões e opiniões sobre o desenvolvimento de software.

E se você tiver interesse em ver uma palestra do autor, eu recomendo este video de uma apresentação que ele fez na sede do Google.

--

--

Elton Minetto
Inside PicPay

Teacher, speaker, Principal Software Engineer @ PicPay. https://eltonminetto.dev. Google Developer Expert in Go