- 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
- número de operadores distintos
- 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
ifereturntem menos operadores e operandos - 4 operadores distintos, 7 operadores no total
- 5 operandos distintos, 6 operandos no total
Volume33.30,Difficulty2.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
Volume71.35,Difficulty3.75- A primeira implementação é visivelmente mais simples, e os valores de
VolumeeDifficultyde 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
- Mesmo em uma função de verificação de par ou ímpar, uma implementação simples com
-
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,reduceefilter, 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 comoMyObj 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,
myObjse tornaa.myObjounull - No segundo,
myObjse tornaa.myObjouundefined - 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
- Em comparação com
-
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 deelse-ifé considerada mais difícil a cadaelse-ifadicional- Isso porque cada ramo de
else-ifpode fazer duas ou mais comparações - Ainda assim, fall-through em
switchebreakausente 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 simplesdebug || (verbose && consoleMode)é mais difícil de ler por causa da mistura de operadoresdebug || !(verbose && consoleMode)fica ainda mais complexo ao incluir negação
-
Tratamento de exceções e goto
try/catchaumenta a dificuldade na Cognitive Complexity, mas vários blocoscatchnão são considerados mais difíceis do que um únicocatch, etryefinallysã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
gotonormalmente é contado como fator de aumento de dificuldade- Ainda assim, formas como
goto outougoto 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,
gotoque cruza fronteiras de laço de um jeito que não pode ser expresso comcontinueoubreakimpõ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
iej,itemeitems, 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,_nodeethisNode, 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
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
mapefiltersem critérioEu 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
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;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()?- 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
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
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
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 pessoasUm matemático humano provavelmente escreveria na forma de uma função por partes, como
'Even'sen mod 2 = 0, caso contrário'Odd', e isso é muito mais claroEla 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
.editorconfigtambém vale a leitura: https://news.ycombinator.com/item?id=43333011Isso reduz discussões sobre detalhes de estilo em pull requests
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
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
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
ife loopsforteriam o mesmo problemaConcordo 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
ife deixar o restante como caminho padrão implícitoif (n % 2 === 0) return "Even"; return "Odd";é mais curto, mas prefiro muito maisif ... 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 comelseé mais intuitivareturn (n % 2 === 0) ? "Even" : "Odd";tem menos boilerplate e, por isso, é mais fácil de lerEm uma linguagem com operador ternário, isso deveria ser fácil de reconhecer
Pela minha experiência,
elsecria 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 contextoDá 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
return "Odd";pareça separado doife, se a linguagem permitir, também adicionaria chaves ao corpo doifHá 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 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 casosEm Rust idiomático,
returnnã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 retornoPython 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-elsecompleta faz com que oreturnindentado pareça uma situação excepcional. Seguindo essa convenção, naturalmente as instruçõesreturnparecem 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 abreakTalvez 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 enlouquecerO 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 deixarcheckedDoggosherdar o tipo da funçãoEstou 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 valorEm JavaScript, você não consegue ter certeza e precisa ficar subindo e descendo pela pilha, mantendo tudo na cabeça
É 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
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 ladoO exemplo de
?.parece um diagnóstico muito específico da linguagem, apontando para um design difícil de ler em JavaScript. Em muitas linguagens,nulleundefinednã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
//TODOem amareloEu 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 fundoEles 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
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
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
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
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