2 pontos por GN⁺ 2025-03-12 | 1 comentários | Compartilhar no WhatsApp
  • Mesmo quando o código é logicamente excelente, ele pode ser cansativo de ler por muito tempo, e este texto encontra essa fadiga na complexidade visível aos olhos
  • As Halstead Complexity Metrics contam a quantidade de operadores e operandos para calcular Volume e Difficulty, e consideram mais difícil uma implementação com mais variáveis e operadores, mesmo que faça a mesma coisa
  • A Cognitive Complexity da SonarSource avalia o peso de condicionais, laços, tratamento de exceções, combinações de operadores lógicos, recursão e goto, com foco em sintaxe abreviada, interrupções do fluxo linear e fluxo de controle aninhado
  • No aspecto das variáveis, variable shadowing, nomes parecidos, longos intervalos de vida das variáveis e padrões de uso pouco familiares aumentam o custo de rastrear o fluxo de dados para quem lê
  • Funções pequenas, padrões familiares, agrupamento de cadeias longas, condicionais simples, uso restrito de goto, aninhamento raso, nomes de variáveis distintos e intervalos de vida curtos das variáveis se tornam critérios de legibilidade aplicáveis além da linguagem e da formatação

Limites e objetivos das métricas de legibilidade de código

  • Não existe uma métrica única amplamente usada e consensual para legibilidade de código
  • Materiais de referência úteis tendem a ser artigos acadêmicos pouco usados na prática ou opiniões, então havia necessidade de uma ferramenta de discussão mais concreta para uso direto em code reviews
  • O objetivo não era inventar uma nova métrica, mas reunir padrões visuais que qualquer pessoa possa usar ao discutir se um código é fácil de ler
  • As medições ou ideias analisadas precisavam atender às seguintes condições
    • Ser aplicáveis a trechos de código-fonte ou a uma única função
    • Não se concentrar apenas na complexidade central que é difícil separar do próprio algoritmo implementado, como a Cyclomatic Complexity
    • Não ficar apenas em elementos superficiais de estilo, como comprimento de nomes de variáveis, espaços em branco, indentação e posicionamento de parênteses

Halstead Complexity Metrics

  • Maurice Halstead propôs, no fim dos anos 1970, as Halstead Complexity Metrics para criar medidas empíricas de código-fonte
  • Essa métrica pode ser aplicada além de linguagem e plataforma, e foca mais em como o código foi escrito do que no algoritmo implementado em si
  • As medidas centrais são quatro contagens baseadas em operadores e operandos
    • número de operadores distintos n1
    • número de operandos distintos n2
    • número total de operadores N1
    • número total de operandos N2
  • Com base nisso, Halstead criou métricas relacionadas como length, volume e difficulty, e tentou até derivar valores para estimar a quantidade de bugs incluídos na implementação
  • Intuitivamente, quanto mais operadores houver, mais interações potenciais precisam ser consideradas, e quanto mais operandos houver, mais difícil fica entender as possibilidades de fluxo de dados
  • Exemplo em JavaScript

    • Mesmo em uma função de verificação de par ou ímpar, uma implementação simples com if e return tem menos operadores e operandos
    • 4 operadores distintos, 7 operadores no total
    • 5 operandos distintos, 6 operandos no total
    • Volume 33.30, Difficulty 2.50
    • Uma implementação que usa array, Number, expressão de comparação e índice tem mais operadores e operandos
    • 7 operadores distintos, 10 operadores no total
    • 9 operandos distintos, 12 operandos no total
    • Volume 71.35, Difficulty 3.75
    • A primeira implementação é visivelmente mais simples, e os valores de Volume e Difficulty de Halstead também sustentam isso
    • A desvantagem é que nem sempre fica claro, em todas as linguagens, o que deve ser considerado operator e operand; para medir, é melhor definir uma ferramenta ou implementação específica e usá-la com consistência
  • Padrões práticos extraídos de Halstead

    • Em geral, funções pequenas e com poucas variáveis são mais fáceis de ler
    • Operadores específicos da linguagem ou syntax sugar impõem carga extra ao leitor, então é melhor não abusar deles
    • Encadear por muito tempo componentes funcionais como map, reduce e filter, além de lambdas, iteradores e comprehensions, pode prejudicar a legibilidade, mesmo quando o código fica conciso
    • Essas cadeias longas podem aparecer com mais frequência em JavaScript e Rust, ou em código Python que mergulhou fundo em itertools

A dificuldade de leitura vista pela Cognitive Complexity

  • A Cognitive Complexity, criada pela SonarSource, é uma métrica pensada para capturar com mais precisão a dificuldade de leitura
  • A ideia central dessa métrica tem três pontos
    • Sintaxe abreviada que combina instruções reduz a dificuldade
    • A dificuldade aumenta sempre que se sai do fluxo linear
    • Fluxo de controle aninhado aumenta a dificuldade
  • Há críticas de que o nome soa como se fosse uma métrica científica ou objetiva, mas na prática ela pode ser vista como uma heurística eficaz
  • O problema da densidade de sintaxe abreviada

    • Em comparação com if (a != null) { myObj = a.myObj; }, uma sintaxe abreviada como MyObj myObj = a?.myObj; é mais curta e leva menos tempo para ser lida
    • Porém, os dois códigos podem não ser exatamente iguais na prática
    • No primeiro, myObj se torna a.myObj ou null
    • No segundo, myObj se torna a.myObj ou undefined
    • Mesmo em linguagens com checagem de tipos forte, como TypeScript ou Rust, isso apenas reduz a chance de omissões, sem garantir que todos os casos sejam tratados corretamente
    • Em JavaScript comum, onde o suporte de checagem de tipos é mais fraco, a chance de esses corner cases não serem tratados é maior
    • Sintaxe abreviada pode ser fácil de escrever e de ler, mas há um trade-off entre concisão e densidade
  • Elementos que quebram o fluxo linear

    • Código linear sem condicionais é mais fácil de escanear do que código com condicionais
    • A Cognitive Complexity considera como fatores de aumento de dificuldade não só condicionais, laços e goto, mas também macros condicionais, try/except, sequências de operadores lógicos e recursão
    • switch é contado como um único grupo, mas uma cadeia de else-if é considerada mais difícil a cada else-if adicional
    • Isso porque cada ramo de else-if pode fazer duas ou mais comparações
    • Ainda assim, fall-through em switch e break ausente também podem aumentar a dificuldade de leitura
    • Dentro de uma condição, encadear o mesmo operador lógico e misturar &&, || e ! resultam em dificuldades diferentes
    • debug || verbose || consoleMode é uma condição simples
    • debug || (verbose && consoleMode) é mais difícil de ler por causa da mistura de operadores
    • debug || !(verbose && consoleMode) fica ainda mais complexo ao incluir negação
  • Tratamento de exceções e goto

    • try/catch aumenta a dificuldade na Cognitive Complexity, mas vários blocos catch não são considerados mais difíceis do que um único catch, e try e finally são ignorados
    • O ato de lançar exceções também pode gerar custo de leitura
    • Quando o tratamento de exceções atravessa fronteiras entre funções, a complexidade das funções envolvidas passa a se entrelaçar
    • Quem lê precisa descobrir onde aquela exceção é capturada
    • goto normalmente é contado como fator de aumento de dificuldade
    • Ainda assim, formas como goto out ou goto done, usadas para liberar recursos em condições de erro e sair da função, são vistas por alguns especialistas como potencialmente úteis
    • Em contraste, goto que cruza fronteiras de laço de um jeito que não pode ser expresso com continue ou break impõe alta carga de leitura porque quem lê precisa reconstruir um novo fluxo de controle

Aninhamento e forma da função

  • Se condicionais já são difíceis de ler por si só, condicionais aninhadas são ainda mais difíceis
  • A Cognitive Complexity acrescenta dificuldade adicional por nível de aninhamento além da pontuação do próprio condicional ou laço
  • Essa ideia também aparece com nomes como “Level of Indentation” ou “Bumpy Road”
  • Quando o aninhamento passa de 2 níveis, a leitura fica especialmente difícil, e código com retorno antecipado reduz o aninhamento e fica mais plano de ler
  • Essa métrica não reflete diretamente o tamanho da função, mas, com todo o resto igual, funções longas exigem mais esforço de leitura do que funções curtas

Nomes de variáveis, intervalo de vida e padrões familiares

  • Nomes distintos e descritivos

    • Nomes descritivos são importantes para entender o que o código tenta fazer, e nomes redundantes ou enigmáticos têm o efeito oposto
    • Variable shadowing é arriscado
    • Deve-se evitar situações em que quem lê precisa seguir as regras de escopo para distinguir qual variável está sendo usada
    • Também é melhor evitar identificadores visualmente parecidos
    • Nomes como i e j, item e items, que se confundem facilmente à vista, podem provocar erros
    • Código que usa várias variações do mesmo nome de variável dentro de uma função, como node, _node e thisNode, tende a ser uma forma propensa a bugs
  • Intervalo de vida curto das variáveis

    • Live variable analysis observa o intervalo entre o primeiro ponto em que a variável é usada e o último ponto em que ela ainda pode ser usada
    • Quando o intervalo de vida da variável é longo, quem lê precisa manter mais variáveis e valores possíveis na cabeça
    • Em vez de declarar todas as variáveis no topo da função, declará-las logo antes do uso real pode reduzir esse intervalo
    • O pior caso é quando uma variável permanece viva ao longo de várias funções e é usada em múltiplos lugares
    • Se o intervalo de vida de várias variáveis for praticamente o mesmo, um objeto pode ser mais apropriado
    • Se objeto não for a escolha certa, é melhor minimizar a quantidade de funções e linhas que o leitor precisa percorrer para entender os valores
  • Cadeias longas e variáveis intermediárias

    • O estilo de programação funcional encurta o intervalo de vida das variáveis, mas cadeias longas demais ou callbacks aninhados podem tornar a leitura pesada
    • Dividir longas cadeias de funções em pequenos grupos e usar variáveis intermediárias bem nomeadas ou funções auxiliares reduz a carga cognitiva de quem lê
    • A versão com variáveis intermediárias pode ser ligeiramente menos eficiente
    • A menos que uma ferramenta de performance mostre que essa linha é realmente um gargalo, essas pequenas diferenças de eficiência não importam
  • Reutilização de padrões de código familiares

    • Reutilizar formas familiares de código e variáveis torna a leitura menos cansativa porque o leitor reconhece padrões que já conhece
    • Isso se conecta ao Principle of Least Surprise
    • Dentro de uma codebase, vale manter consistentes formas recorrentes, como o jeito de escrever condicionais
    • Quando for necessário fugir do padrão, a diferença pode ser destacada com nomes de variáveis ou comentários
    • Levando essa ideia até o fim, isso aponta para o uso de funções template ou genéricas para que o leitor não precise reconhecer novamente padrões repetidos

8 padrões visuais para melhorar a legibilidade

  • Line/Operator/Operand count: funções menores e com menos variáveis e operadores são mais fáceis de ler
  • Novelty: evitar novidade na forma da função, nos operadores e no syntax sugar, reutilizando os padrões comuns da codebase
  • Grouping: dividir longas cadeias de funções, iteradores e comprehensions em grupos lógicos com funções auxiliares ou variáveis intermediárias
  • Conditional simplicity: manter condicionais o mais curtas possível e, dentro de uma mesma condição, preferir sequências do mesmo operador lógico em vez de misturar operadores diferentes
  • Gotos: não usar goto, a menos que siga um padrão específico de tratamento de erro e que as alternativas sejam piores
  • Nesting: minimizar lógica aninhada e grandes mudanças de indentação; se aninhamento profundo for necessário, separar em outra função em vez de enterrá-lo dentro de uma função grande
  • Variable distinction: usar nomes de variáveis descritivos e visualmente distintos, evitando variable shadowing
  • Variable liveness: manter curtos os intervalos de vida das variáveis, especialmente quando atravessam fronteiras de função

Problemas observados em codebases reais

  • As codebases que mais geravam fadiga mental reuniam vários antipadrões ao mesmo tempo
  • Em termos concretos, havia funções longas, mistura de diversos recursos da linguagem e muitas cadeias de funções que deveriam ter sido separadas em helpers
  • Como resultado, a complexidade de aninhamento e longos intervalos de vida de variáveis aumentavam juntos dentro de funções grandes
  • Mesmo com alta qualidade do código e de quem o escreveu, foi encontrado um ou mais bugs críticos
  • Um deles era um bug fácil de ver, mas provavelmente passou despercebido por estar no meio de uma função longa e complexa, o que dificultava o raciocínio
  • Como a pessoa com maior probabilidade de ler mais o seu código daqui a um mês é você mesmo, escrever de forma fácil de ler está diretamente ligado a custo real de trabalho

1 comentários

 
GN⁺ 2025-03-12
Opiniões no Hacker News
  • O artigo diz que cadeias de map/reduce/filter longas ou múltiplas prejudicam a legibilidade, mas isso não decorre de nada do restante do texto
    Parece aquela reclamação comum de dizer que algo é ruim só porque a pessoa não está acostumada, inserida meio de fininho; com um pouco de familiaridade, muitas vezes é muito mais fácil de ler e escrever do que as alternativas
    Por exemplo, acho difícil apresentar um código mais legível do que books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()
    Mesmo olhando por métricas de complexidade, esse tipo de código é melhor do que quase qualquer outra abordagem, e as pessoas deveriam aprender ao menos o básico de programação funcional. Não é preciso explicar mônadas, mas é preciso ter familiaridade suficiente para não sair desqualificando map e filter sem critério

    • A formulação soa desnecessariamente agressiva, mas o exemplo pretendido pela frase citada não era uma cadeia curta dessas
      Eu diria que algo começa a ficar difícil de ler mais ou menos a partir de uma cadeia com 5 chamadas em sequência, e o exemplo do artigo também tinha mais ou menos esse tamanho
      “multiple” se referia a casos com cadeias aninhadas ou em que o tipo sendo manipulado muda; nesses casos, a leitura fica mais lenta
      Componentes funcionais podem ser elegantes, mas também podem ser usados em excesso
    • O exemplo é só um filtro conceitualmente simples sobre uma única lista
      Quando a cadeia fica longa demais, as condições ficam complexas e há muitas listas/variáveis, torna-se difícil entender tudo de uma vez
      Em um loop procedural, dá para dar nomes aos resultados intermediários, e esses nomes permitem esquecer o processamento feito antes e se concentrar na etapa seguinte
    • SELECT DISTINCT author FROM books WHERE pageCount > 1000;
    • O exemplo é bom, mas acho que a reclamação original era mais próxima de cadeias muito longas
      Pessoalmente, costumo dividir a cadeia para dar nomes aos resultados intermediários, como se os nomes das variáveis funcionassem como comentários
      var longBooks = books.filter(book => book.pageCount > 1000)
      var authorsOfLongBooks = longBooks.map(book => book.author).distinct()
    • As soluções em SQL que as pessoas postaram também são boas, mas em Prolog também daria para escrever assim
      ?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).
      Dependendo da estrutura do banco de dados em Prolog, pode ficar ainda mais curto
      ?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).
  • Acho que um bom código tem, fundamentalmente, um lado qualitativo e literário bastante grande
    Programadores e acadêmicos acostumados ao pensamento matemático, que querem respostas quantitativas, muitas vezes se sentem desconfortáveis com isso
    Gosto tanto de Dostoyevsky quanto de Wodehouse; ambos são excelentes, mas de maneiras muito diferentes
    Mesmo que programar não seja um campo tão aberto assim, já trabalhei com boas bases de código que davam impressões qualitativamente muito diferentes, e muitas vezes leva tempo até você “pegar” o estilo de uma base de código, como quando se acostuma à voz de um novo autor

    • Concordo 100%. Um dos melhores elogios que já recebi relacionado a programação foi que “o código é lido como uma história
      A ideia era que eu tinha organizado a ordem das funções para que, ao ler o arquivo de cima para baixo, a narrativa fluísse naturalmente, e tinha criado uma implementação declarativa que parecia conversar com o leitor
      Sigo o paradigma de programação funcional pura, e acho que essa abordagem combina melhor com um estilo mais narrativo
      Como as dependências/entradas de uma função ficam limitadas a argumentos ou a outras funções puras, e a saída fica contida apenas no tipo de retorno, é mais fácil guiar a complexidade passo a passo
      Ao contrário de outros paradigmas que têm complexidades como estado oculto, ironicamente acho que o paradigma mais matematicamente preciso também é o que melhor se adapta a um estilo mais narrativo
    • Se levar mais de 5 segundos para ler e entender o objetivo de alto nível de uma função, considero código ruim
      Não importa a aparência: se, sem uma longa experiência de desenvolvimento, não dá para saber em um tempo razoável o que a função faz, então é simplesmente código ruim
    • Muitos padrões de sintaxe considerados elegantes, na prática, parecem menos claros até do que matemática
      Por exemplo, o operador ternário citado no artigo, return n % 2 === 0 ? 'Even' : 'Odd';, parece ser lido ao contrário pelo cérebro humano, e é mais adequado para o compilador processar a árvore sintática do que para pessoas
      Um matemático humano provavelmente escreveria na forma de uma função por partes, como 'Even' se n mod 2 = 0, caso contrário 'Odd', e isso é muito mais claro
    • É por isso que revisão de código é importante
      Ela ajuda novos integrantes da equipe a aprenderem um estilo consistente e também mantém o estilo da equipe razoavelmente uniforme
      O comentário sobre .editorconfig também vale a leitura: https://news.ycombinator.com/item?id=43333011
      Isso reduz discussões sobre detalhes de estilo em pull requests
    • Talvez seja por isso que Literate Programming não tenha se estabelecido amplamente
  • O artigo é bom, mas acho que deixou passar o fator que mais causa cansaço mental ao ler código: mutabilidade
    Poder “fixar” o significado de uma variável uma única vez ao ler um método, e mantê-lo assim enquanto se infere o restante, é um grande presente
    A compreensão de um método deveria crescer monotonamente de 0% a 100%, sem que você precise reiniciar o método na cabeça porque ficou confuso sobre como o corpo do loop alterou o acumulador em uma determinada iteração
    O verdadeiro motivo pelo qual GOTO é prejudicial também está aqui. O difícil não é mover o ponteiro de instruções mental dentro do método; o problema é que, quando há GOTO, é difícil saber o estado das variáveis mutáveis

    • Discordo. Existe um espaço de informação abstrato que o código modela, e o ponteiro de instruções mental precisa se mover dentro desse espaço
      Tanto variáveis mutáveis quanto imutáveis podem ajudar ou atrapalhar esse movimento, dependendo de quão claramente o código corresponde a esse espaço
      Variáveis imutáveis têm uma pequena vantagem tática, porque não há preocupação de que o valor mude ou mude de uma forma enganosa, mas, pela minha experiência, isso não é grande o bastante para virar uma regra de “use sempre imutabilidade”
      Às vezes, a mutabilidade permite expressar esse espaço de informação de forma muito mais limpa
    • A complexidade total não é apenas uma questão de mover o ponteiro de instruções a partir de um ponto inicial conhecido
      Do ponto de vista do chamado, não do ponto de chamada, quando alguém pode saltar para uma linha específica, não há como rastrear o que aconteceu antes. Como pode ter vindo de qualquer lugar, é necessária uma análise global do programa, não uma análise local
      Se a mutabilidade fosse a verdadeira fonte da complexidade do GOTO, instruções if e loops for teriam o mesmo problema
      Concordo que mutabilidade e estado criam complexidade diretamente, mas vejo GOTO como uma categoria completamente diferente e muito mais nociva
  • Um padrão de que pessoalmente não gosto é retornar direto no if e deixar o restante como caminho padrão implícito
    if (n % 2 === 0) return "Even"; return "Odd"; é mais curto, mas prefiro muito mais if ... return "Even"; else return "Odd";
    O motivo é que o primeiro dá uma sensação assimétrica. "Even" e "Odd" são escolhas simétricas, então a versão com else é mais intuitiva

    • Escrever como return (n % 2 === 0) ? "Even" : "Odd"; tem menos boilerplate e, por isso, é mais fácil de ler
      Em uma linguagem com operador ternário, isso deveria ser fácil de reconhecer
    • Cláusulas de guarda/retornos antecipados tendem a deslocar o foco do desenvolvedor do caminho normal original para o estreitamento do comportamento da função
      Pela minha experiência, else cria aninhamento extra e tende a fazer com que continuemos avaliando condições de borda ou variáveis além do escopo imediato do caminho normal. É preciso carregar todo esse contexto
    • Pessoalmente, prefiro a primeira forma
      Dá para ver visualmente o retorno logo abaixo do nome da função, com um nível de indentação, e passa a sensação de que há um resultado garantido se não houver encerramento antecipado
      Quando o retorno fica enterrado mais abaixo, parece meio estranho
    • Eu colocaria uma linha em branco para que return "Odd"; pareça separado do if e, se a linguagem permitir, também adicionaria chaves ao corpo do if
      Há situações em que permito else, mas geralmente é quando há efeitos colaterais; normalmente, refatorar até eliminá-lo deixa tudo mais claro
      É comum código complexo se transformar em uma sequência de guardas que sai na ordem de importância ou custo de execução, separando a lógica real da função/método das condições de término
    • A assimetria fica clara se você refatorar o código para estilo continuation/callback ou para uma forma mais complexa de mutação de estruturas de dados
      A primeira forma flui para baixo e executa o segundo conjunto de instruções. return é um operador especial que interrompe o fluxo de controle, então o fluxo de controle normal da primeira forma não captura corretamente a completude dos dois casos
      Em Rust idiomático, return não é usado a menos que seja um caso excepcional que quebra o fluxo de controle do método, e o segundo exemplo normalmente aparece com mais frequência sem uma instrução de retorno
      Python também costuma usar retorno antecipado no começo para argumentos ou estados inválidos, com o retorno em posição final sendo o valor de retorno real
      Por causa dessas convenções, quebrar uma estrutura if-else completa faz com que o return indentado pareça uma situação excepcional. Seguindo essa convenção, naturalmente as instruções return parecem redundantes, exceto quando interrompem o fluxo de controle, e a convenção de Rust passa a fazer sentido. Em todas as linguagens, return é uma instrução equivalente a break
  • Talvez seja só comigo, mas TypeScript às vezes torna o código difícil de ler
    Tudo bem se você mantiver o modelo de dados razoavelmente “atômico” e for diligente em declarar e documentar os tipos de fato
    Mas, quando tipos começam a ser derivados de outros tipos por meio de utility types, e você omite tipos explícitos confiando em inferência, a coisa começa a desandar rapidamente
    Fica muito difícil rastrear de onde um campo veio em uma pilha profunda, como 4 ou 5 níveis de indireção entre tipos. Alguns são inferidos, outros explícitos, alguns são tipos derivados, e também há aliases de campos misturados
    Em modelos de dados grandes e pilhas de chamadas profundas, uma forma que omite o tipo de retorno, como function checkDogs(dogs: Dog[]) { ... }, fica completamente inutilizável e é de enlouquecer

    • Acho que funções provavelmente deveriam explicitar o tipo de saída
      O principal motivo é forçar todos os caminhos retornados por aquela função a respeitarem esse tipo
      Já vi muitas regressões acontecerem quando alguém adicionou uma nova condição e retornou um tipo ligeiramente diferente de outro ramo
      Por outro lado, não vejo muito valor em colocar tipos em declarações de variáveis
      No exemplo, const checkedDoggos = checkDogs([]) está bom, e basta deixar checkedDoggos herdar o tipo da função
      Estou lidando com uma base de código em que o linter força const checkedDoggos: DogBreedAndSize[] = checkDogs([]), e isso é bem ridículo e quase não agrega valor
    • Em TypeScript, mesmo que parte do tipo de retorno seja inferida, prefiro ter ao menos alguma informação de tipo a não ter nenhuma informação de tipo, como em JavaScript
      Em JavaScript, você não consegue ter certeza e precisa ficar subindo e descendo pela pilha, mantendo tudo na cabeça
    • Minha regra é só adicionar tipos quando o compilador TypeScript reclamar
  • É preciso ter cuidado com a afirmação de que “funções pequenas com poucas variáveis geralmente são fáceis de ler”
    Não gosto quando a discussão sobre legibilidade se concentra apenas na legibilidade microscópica. Isso facilita a fragmentação excessiva do código sob a suposição equivocada de que a legibilidade microscópica é mais importante que a macroscópica
    Esse tipo de dogmatização cria programadores que só veem as árvores e não a floresta, e acaba gerando código excessivamente ineficiente ou difícil de depurar
    Linguagens da família APL ficam no extremo oposto, mas o ponto ideal real provavelmente está em algum lugar no meio e deve variar bastante de pessoa para pessoa

    • Especialmente quando vários arquivos se entrelaçam, existe claramente um meio-termo
      Em código desconhecido, depois de pular para a definição 3 ou 4 vezes, a coisa já fica pesada; talvez seja um problema meu, mas é difícil imaginar que a maioria das pessoas seja muito melhor do que eu nisso
      Na cultura .NET, especialmente em “clean architecture”, esse problema chega a ser chocante. Quando se tenta modificar uma funcionalidade ou rastrear um problema, ele está espalhado por 4 camadas e 15 arquivos, e alguns arquivos têm mais de 60% só de palavras-chave
      Não sei onde traçar a linha, mas, em geral, prefiro uma função longa que siga outras recomendações e possa ser lida sequencialmente a um código tão fragmentado que obriga a rolar para cima e para baixo a cada 5 linhas
      O mesmo vale para tipos/classes: não há necessidade de colocar em outro arquivo um enum com 4 valores usado apenas neste DTO
  • É um artigo interessante, mas não satisfatório
    Ele salta rápido demais para as conclusões e volta novamente ao gosto pessoal. Concordo com várias preferências, mas o próprio artigo era explicitamente uma tentativa de ir além de preferências
    A ideia de que “operadores específicos de uma linguagem ou açúcar sintático impõem um ônus ao leitor, portanto devem ser evitados” não decorre das métricas. Se uma função tem 3 operadores diferentes e um operador específico da linguagem substitui os três de uma vez, o “esforço” da função diminui
    Componentes como map/reduce/filter, quando bem usados, também podem substituir outros operadores e reduzir o “volume”, então isso pode ir para qualquer lado
    O exemplo de ?. parece um diagnóstico muito específico da linguagem, apontando para um design difícil de ler em JavaScript. Em muitas linguagens, null e undefined não são separados, e ele costuma ser chamado de operador null-safe
    “Variable shadowing é horrível” e “tempos de vida longos fazem você manter mais variáveis na cabeça” podem entrar em conflito
    Em certos contextos, gosto bastante de shadowing, porque ele remove a instância anterior do escopo em vez de deixá-la continuamente acessível

  • Existe uma ótima extensão chamada Highlight para VS Code
    Ela permite aplicar outras cores ao código usando regexes personalizadas, e um uso comum provavelmente é deixar //TODO em amarelo
    Eu a uso para deixar logs mais apagados, porque, ao colocar logs por toda parte, o ruído visual fica grande
    A biblioteca que mantenho usa logs como this.logger?.info('Some logs here');, e aplico opacidade 0.4 neles para fazê-los recuar para o fundo
    Eles ainda ficam visíveis, mas, à primeira vista, a lógica de negócio real se destaca mais
    A configuração pode ser adaptada assim: "highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }
    https://marketplace.visualstudio.com/items?itemName=fabiospa...

  • Não concordo que dividir cadeias longas de funções ou callbacks em pequenos grupos e usar variáveis com bons nomes seja um pouco menos eficiente
    As duas versões podem ser igualmente eficientes
    Nos dois casos, os mesmos objetos são alocados, armazenados no heap e se tornam alvo do garbage collection. A diferença de eficiência depende do compilador
    Na segunda versão, o compilador deveria conseguir ver que cada variável é usada apenas logo depois de ser declarada e tratar esses objetos como fora de escopo, como em uma chamada encadeada

    • Concordo. Depois da compilação, é bem provável que o compilador não se importe com o fato de você ter dado um nome ao valor de retorno
      Claro, partindo do pressuposto de que você deixa o tipo da variável ser inferido
      O custo que se vê na prática aparece quando você materializa explicitamente valores intermediários para vê-los no debugger. Por exemplo, transformá-los em uma lista cria alocações evitáveis e gera custo
  • É boa a tentativa de quantificar “legibilidade”. Precisamos muito mais desse tipo de abordagem
    Hoje, a definição mais comum de legibilidade parece ser algo próximo de “aquilo que eu acho fácil de ler”
    Talvez seja possível encontrar dimensões reais da legibilidade mostrando código a um número muito grande de pessoas, pedindo que escolham uma frase que explique o que ele faz e medindo o tempo
    Os problemas que mais pessoas acertarem no menor tempo médio seriam exemplos de código legível no mundo real e, mais importante, ajudariam a identificar práticas realmente difíceis de ler
    Os respondentes provavelmente se agrupariam por eixos como “experiência em programação” e “entende o paradigma X”, e os resultados também poderiam se deslocar com o tempo conforme as modas mudam

    • Uma das dificuldades centrais é que nós aprendemos a ler código
      Aquilo que aprendemos a ler e escrever molda o que sentimos ser fácil de ler
      O que você está tentando fazer, com quem trabalha, o que você já sabia fazer antes de programar, que outras linguagens conhece e muitos outros fatores influenciam
      Depois de colher os frutos mais fáceis — por exemplo, ir além de simplesmente não dar nomes arbitrários, irrelevantes ou enganosos a variáveis —, muitas questões de “legibilidade” talvez acabem sendo sobre formar consenso
      Pode não haver uma resposta correta que transcenda o grupo específico de programadores com quem se pretende trabalhar
    • Não vejo muito valor nisso
      A legibilidade do código é parecida com a legibilidade de uma língua: em geral, é um problema para quem não conhece bem aquela língua e se resolve com tempo
      O verdadeiro problema da programação é a complexidade do código, e isso não pode ser julgado apenas por métricas de trechos individuais de código
      O problema está nas relações entre funções, não nas escolhas de implementação dentro do corpo de uma função
    • Essa abordagem é unidimensional demais
      Descobrir o que o código faz costuma ser fácil; o difícil é modificar esse código ou adicionar uma funcionalidade
      Essa dificuldade surge porque várias camadas de abstração escondem como elas se conectam entre si