Quando o código é admirável?



O tópico do código ideal geralmente causa polêmica entre programadores experientes. Foi ainda mais interessante obter a opinião do diretor de desenvolvimento do Parallels RAS, Igor Marnat. Sob o corte, a opinião de seu autor sobre o tópico declarado. Aproveite!



Como introdução, gostaria de me debruçar sobre a questão de por que decidi escrever este pequeno artigo. Antes de escrever, fiz uma pergunta do título para vários desenvolvedores. Eu trabalhei com a maioria dos caras por mais de cinco anos, com alguns um pouco menos, mas confio no profissionalismo e na experiência deles incondicionalmente. Toda a experiência em desenvolvimento industrial há mais de dez anos, todos trabalham em empresas russas e internacionais, fabricantes de software.

Alguns colegas acharam difícil responder (algumas ainda pensam), outros chamaram um ou dois exemplos de uma só vez. Para aqueles que deram exemplos, fiz uma pergunta esclarecedora - "O que, de fato, causou essa admiração?" As respostas foram consistentes com os resultados da próxima fase da minha pequena pesquisa. Pesquisei na web por respostas a essa pergunta em diferentes formulações próximas ao título do artigo. Todos os artigos responderam aproximadamente da mesma maneira que meus camaradas responderam.

As respostas dos desenvolvedores, bem como a redação dos artigos encontrados, relacionavam-se à legibilidade e estrutura do código, à elegância das construções lógicas, ao uso de todos os recursos das linguagens de programação modernas e a um certo estilo de design.

Quando fiz a pergunta sobre o "código divino" para mim, a resposta veio à tona imediatamente, do subconsciente. Imediatamente pensei em dois exemplos de código com os quais trabalhava há muito tempo (mais de dez anos atrás), mas ainda sinto um sentimento de admiração e reverência. Tendo considerado as razões de admiração de cada um deles, formulei vários critérios, que serão discutidos abaixo. Vou me debruçar sobre o primeiro exemplo de passagem, mas gostaria de analisar o segundo com mais detalhes. A propósito, em um grau variável, todos esses critérios são considerados no manual de cada desenvolvedor " Perfect Code ", de Steve McConnell, mas este artigo é notavelmente mais curto.

Exemplo dos anos 90


O primeiro exemplo que mencionarei refere-se à implementação do protocolo de modem v42bis. Este protocolo foi desenvolvido no final dos anos 80 - início dos anos 90. Uma idéia interessante, incorporada pelos desenvolvedores do protocolo, é a implementação da compactação de informações por fluxo durante a transmissão através de uma linha de comunicação instável (telefone). A diferença entre a compactação de fluxo e a compactação de arquivos é fundamental. Ao compactar arquivos, o arquivador tem a capacidade de analisar completamente o conjunto de dados, determinar a abordagem ideal para compactar e codificar dados e gravar todos os dados no arquivo sem se preocupar com a possível perda de dados e metadados. Ao descompactar, por sua vez, o conjunto de dados fica totalmente acessível novamente, a integridade é garantida por uma soma de verificação. Com a compressão em linha, o arquivador pode acessar apenas uma pequena janela de dados, não há garantia de perda de dados, é comum a necessidade de reinstalar a conexão e inicializar o processo de compactação.

Os autores do algoritmo encontraram uma solução elegante, cuja descrição ocupa literalmente várias páginas . Muitos anos se passaram, mas ainda estou impressionado com a beleza e a graça da abordagem proposta pelos desenvolvedores do algoritmo.

Este exemplo ainda não se refere ao código como tal, mas ao algoritmo, portanto, não iremos nos aprofundar nele com mais detalhes.

Linux é a cabeça de tudo!


Eu gostaria de analisar o segundo exemplo de um código perfeito com mais detalhes. Este é o código do kernel do Linux. O código que, no momento da redação deste documento, controla a operação de 500 supercomputadores dos 500 primeiros , o código que roda em todos os segundos telefones do mundo e que controla a maioria dos servidores na Internet.

Por exemplo, considere o arquivo memory.c do kernel do Linux , que pertence ao subsistema de gerenciamento de memória.

1. As fontes são fáceis de ler. Eles são escritos usando um estilo muito simples, fácil de seguir e difícil de se confundir. Os caracteres maiúsculos são usados ​​apenas para diretivas e macros do pré-processador; todo o resto é escrito em letras minúsculas; as palavras nos nomes são separadas por sublinhados. Este é talvez o estilo de codificação mais fácil possível, exceto pela falta de um estilo. Ao mesmo tempo, o código é perfeitamente legível. A abordagem de indentação e comentários é visível em qualquer parte de qualquer arquivo do kernel, por exemplo:

static void tlb_remove_table_one(void *table) {     /*      * This isn't an RCU grace period and hence the page-tables cannot be      * assumed to be actually RCU-freed.      *      * It is however sufficient for software page-table walkers that rely on      * IRQ disabling. See the comment near struct mmu_table_batch.      */     smp_call_function(tlb_remove_table_smp_sync, NULL, 1);     __tlb_remove_table(table); } 


2. Não há muitos comentários no código, mas esses geralmente são úteis. Como regra geral, eles descrevem não uma ação que já é óbvia a partir do código (um exemplo clássico de um comentário inútil é "cnt ++; // contador de incremento"), mas o contexto dessa ação - por que aqui é feito o que é feito, por que é feito, por que aqui, com que suposições é usada, com que outros lugares no código está conectado. Por exemplo:

 /** * tlb_gather_mmu - initialize an mmu_gather structure for page-table tear-down * @tlb: the mmu_gather structure to initialize * @mm: the mm_struct of the target address space * @start: start of the region that will be removed from the page-table * @end: end of the region that will be removed from the page-table * * Called to initialize an (on-stack) mmu_gather structure for page-table * tear-down from @mm. The @start and @end are set to 0 and -1 * respectively when @mm is without users and we're going to destroy * the full address space (exit/execve). */ void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,            unsigned long start, unsigned long end) 


Outro uso de comentários no kernel é descrever o histórico de alterações, geralmente no início de um arquivo. A história do kernel existe há quase trinta anos, e é interessante ler alguns lugares, você se sente parte da história:

 /* * demand-loading started 01.12.91 - seems it is high on the list of * things wanted, and it should be easy to implement. - Linus */ /* * Ok, demand-loading was easy, shared pages a little bit tricker. Shared * pages started 02.12.91, seems to work. - Linus. * * Tested sharing by executing about 30 /bin/sh: under the old kernel it * would have taken more than the 6M I have free, but it worked well as * far as I could see. * * Also corrected some "invalidate()"s - I wasn't doing enough of them. */ 


3. O código do kernel usa macros especiais para verificar os dados. Eles também são usados ​​para verificar o contexto em que o código funciona. A funcionalidade dessas macros é semelhante à afirmação padrão, com a diferença de que o desenvolvedor pode substituir a ação que é executada se a condição for verdadeira. Uma abordagem geral para o processamento de dados no kernel - tudo o que vem do espaço do usuário é verificado; no caso de dados errados, o valor correspondente é retornado. Nesse caso, WARN_ON pode ser usado para emitir um registro no log do kernel. BUG_ON é geralmente muito útil ao depurar um novo código e iniciar o kernel em novas arquiteturas.

A macro BUG_ON geralmente faz com que o conteúdo dos registradores e da pilha seja impresso e interrompe todo o sistema ou o processo no contexto em que ocorreu a chamada correspondente. A macro WARN_ON simplesmente envia uma mensagem para o log do kernel se a condição for verdadeira. Há também macros WARN_ON_ONCE e várias outras, cuja funcionalidade é clara no nome.

 void unmap_page_range(struct mmu_gather *tlb, ….     unsigned long next;    BUG_ON(addr >= end);    tlb_start_vma(tlb, vma); int apply_to_page_range(struct mm_struct *mm, unsigned long addr, …    unsigned long end = addr + size;    int err;    if (WARN_ON(addr >= end))        return -EINVAL; 


A abordagem na qual os dados obtidos de fontes não confiáveis ​​são verificados antes do uso e a resposta do sistema a situações "impossíveis" é fornecida e determinada, simplificando significativamente a depuração e operação do sistema. Você pode considerar essa abordagem como uma implementação do princípio de falha antecipada e em voz alta.

4. Todos os principais componentes do kernel fornecem aos usuários informações sobre seu status por meio de uma interface simples, o sistema de arquivos virtual / proc /.

Por exemplo, informações de status da memória estão disponíveis no arquivo / proc / meminfo

 user@parallels-vm:/home/user$ cat /proc/meminfo MemTotal:    2041480 kB MemFree:      65508 kB MemAvailable:   187600 kB Buffers:      14040 kB Cached:      246260 kB SwapCached:    19688 kB Active:     1348656 kB Inactive:     477244 kB Active(anon):  1201124 kB Inactive(anon):  387600 kB Active(file):   147532 kB Inactive(file):  89644 kB …. 


As informações acima são coletadas e processadas em vários arquivos de origem do subsistema de gerenciamento de memória. Portanto, o primeiro campo MemTotal é o valor do campo totalram da estrutura sysinfo, que é preenchida com a função si_meminfo do arquivo page_alloc.c .

Obviamente, organizar a coleta, armazenamento e fornecer ao usuário acesso a essas informações requer esforço do desenvolvedor e alguma sobrecarga do sistema. Ao mesmo tempo, os benefícios de ter acesso conveniente e fácil a esses dados são inestimáveis, tanto no processo de desenvolvimento quanto na operação do código.

O desenvolvimento de praticamente qualquer sistema deve começar com um sistema para coletar e fornecer informações sobre o estado interno do seu código e dados. Isso ajudará bastante no processo de desenvolvimento e teste e, posteriormente, na operação.

Como Linus disse : “Os programadores ruins se preocupam com o código. Bons programadores se preocupam com estruturas de dados e seus relacionamentos ".

5. Todo o código é lido e discutido por vários desenvolvedores antes de confirmar. Um histórico de alterações no código-fonte é gravado e disponível. Alterações em qualquer linha podem ser rastreadas até sua ocorrência - o que mudou, por quem, quando, por que e quais questões foram discutidas pelos desenvolvedores. Por exemplo, a alteração em https://github.com/torvalds/linux/commit/1b2de5d039c883c9d44ae5b2b6eca4ff9bd82dac#diff-983ac52fa16631c1e1dfa28fc593d2ef no código memory.c é inspirada em https://bugzsh_b.bg?hl=pt-BR Uma pequena otimização do código foi feita (uma chamada para ativar a proteção contra gravação na memória não ocorre se a memória já estiver protegida contra gravação).

É sempre importante para um desenvolvedor que trabalha com código entender o contexto em torno desse código, com quais suposições o código foi criado, o que e quando ele foi alterado, a fim de entender quais cenários podem ser afetados pelas alterações que ele fará.

6. Todos os elementos importantes do ciclo de vida do código do kernel estão documentados e acessíveis , começando com o estilo de codificação e terminando com o conteúdo e o cronograma para o lançamento de versões estáveis ​​do kernel . Cada desenvolvedor e usuário que deseja trabalhar com o código do kernel em uma capacidade ou outra possui todas as informações necessárias para isso.

Esses momentos pareciam importantes para mim, basicamente, eles determinaram meu entusiasmo pelo código principal. Obviamente, a lista é muito curta e pode ser expandida. Mas os pontos listados acima, na minha opinião, referem-se a aspectos-chave do ciclo de vida de qualquer código-fonte do ponto de vista do desenvolvedor que trabalha com esse código.

O que eu gostaria de dizer em conclusão. Os desenvolvedores principais são inteligentes e experientes, eles conseguiram. Comprovado por bilhões de dispositivos Linux

Seja um desenvolvedor de kernel, use as práticas recomendadas e leia Code Complete!

Z.Y. A propósito, que critérios de um código ideal você possui pessoalmente? Compartilhe seus pensamentos nos comentários.

Source: https://habr.com/ru/post/pt426679/


All Articles