Hoje quero trazer uma reflexão sobre o papel que o design de código desempenha até os dias de hoje na rotina dos times de desenvolvimento. Será que estamos adotando as melhores práticas para que nosso código realmente facilite os diversos tipos de manutenção que são necessárias no decorrer da vida dele?
Design de software: definições e como chegamos em Design de Código
Na verdade a expressão mais utilizada é Design de software, mas quando vamos procurar alguma fonte mais formal para estabelecer uma linha a ser seguida, percebemos que existe variação na intenção de cada pessoa. Por exemplo, podemos seguir a seguinte definição:
“Software design is the process by which an agent creates a specification of a software artifact intended to accomplish goals, using a set of primitive components and subject to constraints“.
Uma possível tradução para o português seria: “O design de software é o processo pelo qual um agente cria uma especificação de um artefato de software destinado a cumprir objetivos, usando um conjunto de componentes primitivos e sujeito a restrições”. Essa definição foi retirada da publicação “A Proposal for a Formal Definition of the Design Concept” de Paul Ralph e Yair Wand.
Além dessas, várias outras podem ser encontradas. Para facilitar a leitura deste post, vamos trazer uma visão um pouco mais centrada no momento que precisamos, de fato, escrever o código necessário para implementar alguma funcionalidade.
Imagine que você já está seguindo uma ideia de arquitetura, pouco importa qual, e chegou o momento de escrever o código necessário para ter um determinado fluxo funcionando. Como você faz para decidir onde vai cada código? Principalmente quando você tem uma regra de negócio mais complexa, como você faz para escrever essa regra de tal maneira que ela seja considerada mais fácil de ser mantida pela próxima pessoa?
Afinal, o que é design de código?
Por essa perspectiva, podemos andar para frente aqui dando um zoom no Design de Software e chegando no Design de código em si. Nos inspirando na definição acima de Design de software, vamos considerar que:
Design de Código é o processo pelo qual um agente desenvolve um código de modo a atender o requisito funcional também fazendo com que este mesmo código seja possível de ser mantido com o mínimo de dificuldade pelo próximo agente.
A ISO/IEC 25010:2011 define um conjunto de características que podem ser observadas em relação à qualidade interna de um software. É algo bem completo e que abrange diversas áreas que podem afetar a chamada qualidade. Enquanto a ISO olha para diversas características, este post está focado em apenas uma delas: Manutenibilidade.
Definição de manutenibilidade
Podemos chamar manutenibilidade de “capacidade de manutenção” para facilitar o entendimento. Quando falamos de manutenibilidade podemos começar com um resumo trazido pela própria ISO:
“Maintainability can be interpreted as either an inherent capability of the product or system to facilitate maintenance activities, or the quality in use experienced by the maintainers for the goal of maintaining the product or system.”
Em tradução simples temos o seguinte:
“Manutenibilidade pode ser interpretada como uma capacidade inerente do produto ou sistema para facilitar as atividades de manutenção ou a qualidade em uso experimentada pelos mantenedores com o objetivo de manter o produto ou sistema.”
Características da manutenibilidade
A manutenibilidade é subdividida em cinco características:
1. Modularity ou modularidade em português – grau em que um sistema ou programa de computador é composto de componentes de tal modo que uma mudança em um componente tenha impacto mínimo em outros componentes;
2. Reusability ou reusabilidade em português – grau em que uma parte pode ser usada em mais de um sistema ou na construção de outras partes;
3. Analysability ou analisabilidade em português – grau de eficácia e eficiência com o qual é possível avaliar o impacto em um produto ou sistema de uma alteração pretendida em uma ou mais de suas partes, ou diagnosticar um produto quanto a deficiências ou causas de falhas, ou identificar partes a serem modificadas;
4. Modifiability ou modificabilidade em português – grau em que um produto ou sistema pode ser modificado de forma eficaz e eficiente sem introduzir defeitos ou degradar a qualidade do produto existente;
5. Testability ou testabilidade em português – grau de eficácia e eficiência com o qual os critérios de teste podem ser estabelecidos para um sistema, produto ou componente e os testes podem ser realizados para determinar se esses critérios foram atendidos.
Todos os aspectos citados tendem a ser importantes, sem dúvida, poderíamos falar mais sobre cada um. Só que para manter este texto objetivo, vamos focar na quarta característica, a modificabilidade.
Estratégia comum para acomodar modificações
A linha mais comum de design para suportar modificações dentro de um projeto é que devemos escrever o software de tal maneira que a maioria das atividades de manutenção (corretiva, evolutiva etc) devem ser feitas com o mínimo de alteração dentro da base de código existente.
As peças do nosso código devem nascer preparadas para serem trocadas. Essa é a linha de Design que acredita que a flexibilidade do código é o melhor caminho para acomodar qualquer tipo de mudança.
Temos diversas influências para isso, provavelmente a mais forte vem de princípios como Clean Code, SOLID, Domain Driven Design etc. Tudo isso, em geral, deriva da linha de pensamento de David Parnas, um pesquisador hoje aposentado, mas que publicou trabalhos sobre modularização de software.
E o entendimento de cada código escrito, como fica?
Kent Beck, outra pessoa com grande influência na comunidade de software, certa vez escreveu o seguinte tuíte:
“The goal of software design is to create chunks or slices that fit into a human mind. The software keeps growing but the human mind maxes out, so we have to keep chunking and slicing differently if we want to keep making changes.”
Em tradução simples temos:
“O objetivo do design de software é criar pedaços ou fatias que se encaixem na mente humana. O software continua crescendo, mas a mente humana atinge seu limite máximo, então temos que continuar a fragmentar e fatiar de forma diferente se quisermos continuar fazendo alterações.”
E esse é um problema que acontece recorrentemente, não é mesmo?
Não importa qual o conjunto de inspirações que te guia, em determinado momento você precisa parar e escrever o pedaço de código que atenda determinada demanda. E este código, se possível, precisa ser entendido pela próxima pessoa. Se o entendimento não for facilitado, provavelmente a modificabilidade será prejudicada.
A dificuldade de entendimento do código também é tema de pesquisa. O termo inglês mais usado para essa análise é cognitive load.
Talvez o exemplo mais marcante de códigos que tendem a ser muito difíceis de serem entendidos sejam as chamadas God Classes. E que bom seria se houvesse apenas uma God Class 🙂
O fato é que controlar a complexidade das unidades de código (por exemplo, cada arquivo do projeto) é um problema que aparentemente ainda atrapalha o andamento de um projeto.
Dificuldade de entendimento do código – exemplos e análises do porque isso acontece
Os softwares evoluem, as necessidades mudam e o código precisa sofrer alterações. Durante esse processo, naturalmente o código vai ganhando mais elementos e seu nível de dificuldade de entendimento tende a subir.
Para termos um exemplo e tudo ficar mais palpável, você pode olhar a classe AmazonS3Client do projeto open source aws-sdk-java.
Ela atualmente tem mais de seis mil linhas de código. Só que mais ou menos sete anos atrás, ela tinha menos de quatro mil. Quatro mil já era um número potencialmente alto e ainda colocaram mais três mil linhas de código nela.
Por que será que isso acontece? Algumas hipóteses:
- O nível da equipe de engenharia é mais baixo do que deveria. Será que o nível da equipe da Amazon é mais baixo do que deveria mesmo?
- A pessoa pensa: O código já tem N mil linhas, cinquenta ou cem a mais não vão fazer diferença.
- Do ponto de vista da pessoa, aquele código ainda está ok. Afinal de contas, o entendimento é contextual. O que é difícil para um, não necessariamente é difícil para outro.
- Não existe limite de complexidade estabelecido. Então talvez ainda possa adicionar mais código aqui.
- Existe um limite (raro), mas não é trivial perceber se cheguei ou não.
Este exemplo do código da Amazon é apenas um entre vários outros que encontramos em múltiplos projetos open source. E olha que projeto open source tem time, escopo e pace de desenvolvimento mais estável do que encontramos normalmente no mercado.
Em outras palavras, as boas práticas de hoje não garantem um controle de complexidade efetivo
Manutenibilidade é um aspecto muito importante do ciclo de vida de um produto sustentado por software. As inspirações e técnicas atuais de arquitetura e design parecem desempenhar um bom papel quando falamos de modularização, reuso e até analisabilidade. Também existem maneiras bem estabelecidas quando falamos dos dois vieses citados sobre testabilidade.
Por outro lado, quando olhamos para a modificabilidade, ainda ficamos para trás. Princípios de Design (como, por exemplo, SOLID, Clean Code, Domain Driven Design, técnicas de refatoração, Design para testabilidade etc) parecem que olham ainda para um nível acima do que o necessário para controlar, de fato, a complexidade do código produzido.
Precisamos de algo que direcione, de maneira objetiva, o controle da complexidade.
Provavelmente os conjuntos de métricas formais de software, como por exemplo as sugeridas por Chidamber & Kemerer, formam, até o momento, o melhor jeito de olhar para a complexidade. Existem projetos, como o próprio Sonarqube e o SonarLint que também deveriam auxiliar neste aspecto.
Só que entre as 388 regras de code smell presentes no Sonar Lint, apenas uma atua na “dificuldade de entendimento”. Especificamente quando temos controles de fluxo.
Design de código para reduzir o cognitive load
A ideia aqui era tentar estabelecer que fazer um código que seja “entendível” pela próxima pessoa é uma tarefa complicada. Na opinião desta pessoa que escreve, a principal dificuldade de buscar um código que diminua o cognitive load se deve ao seguinte fato:
“Entendimento é individual. Cada pessoa possui um background diferente referente àquele contexto e isso faz com que cada indivíduo analise de maneira diferente a complexidade do mesmo material.”
Precisamos buscar uma maneira de unificar a visão de complexidade dentro de um time, maximizando a chance que todo mundo olhe para um determinado código e analise a complexidade exatamente da mesma forma.
Dessa forma, podemos caminhar para um controle mais severo do cognitive load e, como consequência, teremos um software com mais chances de ser modificado com sucesso. Mas como fazer isso? É o que você vai ver quando falarmos sobre o conceito de Cognitive-Driven Development (CDD) que vem por aí.