Readability versus maintainability, caso especial de escrita de chamadas de função aninhadas

57

Meu estilo de codificação para chamadas de função aninhadas é o seguinte:

var result_h1 = H1(b1);
var result_h2 = H2(b2);
var result_g1 = G1(result_h1, result_h2);
var result_g2 = G2(c1);
var a = F(result_g1, result_g2);

Eu recentemente mudei para um departamento onde o seguinte estilo de codificação é muito usado:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

O resultado da minha maneira de codificar é que, no caso de uma função de falha, o Visual Studio pode abrir o despejo correspondente e indicar a linha onde o problema ocorre (estou especialmente preocupado com violações de acesso).

Eu temo que, em caso de falha devido ao mesmo problema programado na primeira forma, eu não seja capaz de saber qual função causou o travamento.

Por outro lado, quanto mais processamento você colocar em uma linha, mais lógica você obterá em uma página, o que aumenta a legibilidade.

O meu medo está correto ou estou faltando alguma coisa e, em geral, o que é preferido em um ambiente comercial? Legibilidade ou facilidade de manutenção?

Não sei se é relevante, mas estamos trabalhando em C ++ (STL) / C #.

    
por Dominique 22.02.2018 / 12:27
fonte

9 respostas

111

Se você se sentiu obrigado a expandir um forro como

 a = F(G1(H1(b1), H2(b2)), G2(c1));

Eu não te culpo. Isso não é apenas difícil de ler, é difícil de depurar.

Por quê?

  1. É denso
  2. Alguns depuradores só destacam a coisa toda de uma só vez
  3. É livre de nomes descritivos

Se você expandi-lo com resultados intermediários, você obtém

 var result_h1 = H1(b1);
 var result_h2 = H2(b2);
 var result_g1 = G1(result_h1, result_h2);
 var result_g2 = G2(c1);
 var a = F(result_g1, result_g2);

e ainda é difícil de ler. Por quê? Ele resolve dois dos problemas e introduz um quarto:

  1. É denso
  2. Alguns depuradores só destacam a coisa toda de uma só vez
  3. É livre de nomes descritivos
  4. Está repleto de nomes não descritivos

Se você expandi-lo com nomes que adicionam um novo significado semântico, melhor ainda! Um bom nome me ajuda a entender.

 var temperature = H1(b1);
 var humidity = H2(b2);
 var precipitation = G1(temperature, humidity);
 var dewPoint = G2(c1);
 var forecast = F(precipitation, dewPoint);

Agora pelo menos isso conta uma história. Ele corrige os problemas e é claramente melhor do que qualquer outra coisa oferecida aqui, mas requer que você indique os nomes.

Se você faz isso com nomes sem sentido como result_this e result_that porque você simplesmente não consegue pensar em bons nomes, então eu realmente prefiro que você nos poupe da desordem do nome sem sentido e expanda-o usando alguns espaços em branco bons e antigos:

int a = 
    F(
        G1(
            H1(b1), 
            H2(b2)
        ), 
        G2(c1)
    )
;

É tão legível, se não mais, do que aquele com nomes de resultados sem sentido (não que esses nomes de função sejam tão bons assim).

  1. É denso
  2. Alguns depuradores só destacam a coisa toda de uma só vez
  3. É livre de nomes descritivos
  4. Está repleto de nomes não descritivos

Quando você não consegue pensar em bons nomes, é tão bom quanto parece.

Por alguma razão, depuradores adoram novas linhas então você deve achar que a depuração não é difícil:

Seissonãoforsuficiente,imaginequeG2()foichamadoemmaisdeumlugare,emseguida,issoaconteceu:

Exception in thread "main" java.lang.NullPointerException at composition.Example.G2(Example.java:34) at composition.Example.main(Example.java:18)

Acho legal que, como cada chamada de G2() estaria em sua própria linha, esse estilo leva você diretamente para a chamada incorreta na principal.

Então, por favor, não use os problemas 1 e 2 como desculpa para nos deixar com o problema 4. Use bons nomes quando puder pensar neles. Evite nomes sem sentido quando você não puder.

Lightness Races no Orbit's indica corretamente que essas funções são artificiais e possuem nomes próprios inválidos. Então, aqui está um exemplo de aplicar esse estilo a algum código do mundo real:

var user = db.t_ST_User.Where(_user => string.Compare(domain,  
_user.domainName.Trim(), StringComparison.OrdinalIgnoreCase) == 0)
.Where(_user => string.Compare(samAccountName, _user.samAccountName.Trim(), 
StringComparison.OrdinalIgnoreCase) == 0).Where(_user => _user.deleted == false)
.FirstOrDefault();

Eu odeio olhar para esse fluxo de ruído, mesmo quando a quebra de linha não é necessária. Veja como fica esse estilo:

var user = db
    .t_ST_User
    .Where(
        _user => string.Compare(
            domain, 
            _user.domainName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(
        _user => string.Compare(
            samAccountName, 
            _user.samAccountName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(_user => _user.deleted == false)
    .FirstOrDefault()
;

Como você pode ver, descobri que esse estilo funciona bem com o código funcional que está se movendo para o espaço orientado a objetos. Se você puder criar bons nomes para fazer isso em um estilo intermediário, mais poder terá para você. Até lá estou usando isso. Mas em qualquer caso, por favor, encontre alguma maneira de evitar nomes de resultados sem sentido. Eles fazem meus olhos doerem.

    
por 22.02.2018 / 14:54
fonte
50

On the other hand, the more processing you put on a line, the more logic you get on one page, which enhances readability.

Eu discordo totalmente disso. Basta olhar para os seus dois exemplos de código como incorretos:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

é ouvido para ler. "Legibilidade" não significa densidade de informação; significa "fácil de ler, entender e manter".

Às vezes, o código é simples e faz sentido usar uma única linha. Outras vezes, isso dificulta a leitura, sem nenhum benefício óbvio além de colocar mais em uma linha.

No entanto, eu também gostaria de chamá-lo para afirmar que "fácil diagnosticar falhas" significa que o código é fácil de manter. Código que não falha é muito mais fácil de manter. "Fácil de manter" é alcançado principalmente por meio do código de fácil leitura e compreensão, com um bom conjunto de testes automatizados.

Portanto, se você estiver transformando uma única expressão em uma multi-linha com muitas variáveis apenas porque seu código geralmente falha e você precisa de melhores informações de depuração, pare de fazer isso e torne o código mais robusto. Você deve preferir escrever códigos que não precisem de depuração sobre códigos fáceis de depurar.

    
por 22.02.2018 / 12:45
fonte
25

Seu primeiro exemplo, o formulário de atribuição única, é ilegível porque os nomes escolhidos são totalmente sem sentido. Isso pode ser um artefato de tentar não divulgar informações internas de sua parte, o verdadeiro código pode ser bom a esse respeito, não podemos dizer. De qualquer forma, é prolixo devido à densidade de informações extremamente baixa, o que geralmente não se presta a um entendimento fácil.

Seu segundo exemplo é condensado em um grau absurdo. Se as funções possuírem nomes úteis, isso pode ser bom e bem legível, porque não há muito dele, mas como está, é confuso na outra direção.

Depois de introduzir nomes significativos, você pode verificar se um dos formulários parece natural ou se há um meio dourado para atirar.

Agora que você tem código legível, a maioria dos bugs será óbvia, e os outros pelo menos terão mais dificuldade em se esconder de você.

    
por 22.02.2018 / 13:08
fonte
17

Como sempre, quando se trata de legibilidade, a falha está nos extremos . Você pode aceitar qualquer bom conselho de programação, transformá-lo em uma regra religiosa e usá-lo para produzir um código totalmente ilegível. (Se você não acredita em mim, confira estes dois vencedores IOCCC borsanyi e goren e dê uma olhada Quão diferente eles usam funções para tornar o código totalmente ilegível Sugestão: Borsanyi usa exatamente uma função, goren muito, muito mais ...)

No seu caso, os dois extremos são 1) usando apenas instruções de expressão única, e 2) juntando tudo em declarações grandes, concisas e complexas. Qualquer abordagem levada ao extremo torna o seu código ilegível.

Sua tarefa, como programador, é encontrar um equilíbrio . Para cada declaração que você escreve, é sua tarefa responder à pergunta: "Essa declaração é fácil de entender e serve para tornar a minha função legível?"

O ponto é que não existe uma complexidade de uma única mensuração mensurável que possa decidir o que é bom ser incluído em uma única declaração. Tomemos por exemplo a linha:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

Esta é uma declaração bastante complexa, mas qualquer programador que se preze deve ser capaz de entender imediatamente o que isso significa. É um padrão bastante conhecido. Como tal, é muito mais legível que o equivalente

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

que divide o padrão bem conhecido em um número aparentemente sem sentido de etapas simples. No entanto, a declaração da sua pergunta

var a = F(G1(H1(b1), H2(b2)), G2(c1));

parece muito complicado para mim, mesmo sendo uma operação menor que o cálculo de distância . É claro que isso é uma consequência direta de eu não saber nada sobre F() , G1() , G2() , H1() ou H2() . Eu poderia decidir de forma diferente se eu soubesse mais sobre eles. Mas esse é precisamente o problema: a complexidade aconselhável de uma declaração depende strongmente do contexto e das operações envolvidas. E você, como programador, é quem deve dar uma olhada nesse contexto e decidir o que incluir em uma única declaração. Se você se preocupa com a legibilidade, não pode descarregar essa responsabilidade em alguma regra estática.

    
por 23.02.2018 / 15:07
fonte
14

@Dominique, acho que na análise de sua pergunta, você está cometendo o erro de que "legibilidade" e "capacidade de manutenção" são duas coisas separadas.

É possível ter um código que seja sustentável, mas ilegível? Por outro lado, se o código é extremamente legível, por que se tornaria inatingível por ser legível? Eu nunca ouvi falar de nenhum programador que tenha jogado esses fatores um contra o outro, tendo que escolher um ou outro!

Em termos de decidir usar variáveis intermediárias para chamadas de função aninhadas, no caso de 3 variáveis dadas, chamadas para 5 funções separadas e algumas chamadas aninhadas 3 deep, eu tenderia a usar pelo menos alguns variáveis intermediárias para quebrar isso, como você fez.

Mas eu certamente não chego a dizer que as chamadas de função nunca devem ser aninhadas. É uma questão de julgamento nas circunstâncias.

Eu diria que os seguintes pontos se baseiam no julgamento:

  1. Se as funções chamadas representam operações matemáticas padrão, elas são mais capazes de serem aninhadas do que funções que representam alguma lógica de domínio obscura cujos resultados são imprevisíveis e não podem necessariamente ser avaliados mentalmente pelo leitor.

  2. Uma função com um único parâmetro é mais capaz de participar de um ninho (como uma função interna ou externa) do que uma função com vários parâmetros. Funções de mistura de diferentes arities em diferentes níveis de nidificação são propensas a deixar o código parecido com um ouvido de porco.

  3. Um ninho de funções que os programadores estão acostumados a ver expresso de uma maneira particular - talvez porque ele representa uma técnica matemática padrão ou equação, que tem uma implementação padrão - pode ser mais difícil ler e verificar se está dividido em variáveis intermediárias.

  4. Um pequeno número de chamadas de funções que executam funcionalidades simples e já estão prontas para serem lidas e, em seguida, são divididas excessivamente e atomizadas, é capaz de ser mais difícil de ler do que não foi quebrado em absoluto.

por 22.02.2018 / 15:37
fonte
4

Ambos são sub-ótimos. Considere Comentários.

// Calculating torque according to Newton/Dominique, 4th ed. pg 235
var a = F(G1(H1(b1), H2(b2)), G2(c1));

Ou funções específicas em vez de funções gerais:

var a = Torque_NewtonDominique(b1,b2,c1);

Ao decidir quais resultados devem ser soletrados, tenha em mente o custo (cópia x referência, valor l versus valor r), legibilidade e risco, individualmente para cada declaração.

Por exemplo, não há nenhum valor agregado de mover conversões simples de unidade / tipo para suas próprias linhas, porque elas são fáceis de ler e extremamente improváveis de falhar:

var radians = ExtractAngle(c1.Normalize())
var a = Torque(b1.ToNewton(),b2.ToMeters(),radians);

Em relação à sua preocupação de analisar os despejos de memória, a validação de entrada geralmente é muito mais importante - é bem provável que ocorra uma falha real dentro dessas funções em vez da linha chamá-las e, mesmo que não, você não precisa ser disse exatamente onde as coisas explodiram. É muito mais importante saber onde as coisas começaram a desmoronar, do que saber onde elas finalmente explodiram, o que é o que a validação de entrada captura.

    
por 23.02.2018 / 15:43
fonte
1

A legibilidade é a maior parte da manutenção. Duvida de mim? Escolha um grande projeto em um idioma que você não conhece (de preferência tanto a linguagem de programação quanto a linguagem dos programadores), e veja como você poderia refatorá-lo ...

Eu colocaria a legibilidade entre 80 e 90 de capacidade de manutenção. Os outros 10 a 20% são a capacidade de refatoração.

Dito isto, você efetivamente passa 2 variáveis para sua função final (F). Essas duas variáveis são criadas usando outras três variáveis. Você teria sido melhor passar b1, b2 e c1 para F, se F já existe, então criar D que faz a composição para F e retorna o resultado. Nesse ponto, é apenas uma questão de dar a D um bom nome, e não importa qual estilo você usa.

Em um não relacionado, você diz que mais lógica na página ajuda na legibilidade. Isso é incorreto, a métrica não é a página, é o método, e a lógica LESS que um método contém, mais legível é.

Legível significa que o programador pode manter a lógica (entrada, saída e algoritmo) em sua cabeça. Quanto mais ele faz, menos o programador pode entendê-lo. Leia sobre a complexidade ciclomática.

    
por 23.02.2018 / 03:00
fonte
1

Independentemente de você estar em C # ou C ++, contanto que você esteja em uma compilação de depuração, uma possível solução é encapsular as funções

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Você pode escrever uma expressão on-line e ainda ser apontado onde o problema é simplesmente observando o rastreamento de pilha.

returnType F( params)
{
    returnType RealF( params);
}

É claro que, se você chamar a mesma função várias vezes na mesma linha, não poderá saber qual função, mas você ainda pode identificá-la:

  • Observando os parâmetros de função
  • Se os parâmetros forem idênticos e a função não tiver efeitos colaterais, duas chamadas idênticas se tornarão duas chamadas idênticas, etc.

Esta não é uma bala de prata, mas não é tão ruim a meio caminho.

Sem mencionar que o agrupamento de funções pode até ser mais benéfico para a legibilidade do código:

type CallingGBecauseFTheorem( T b1, C b2)
{
     return G1( H1( b1), H2( b2));
}

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));
    
por 25.02.2018 / 09:45
fonte
1

Na minha opinião, o código de autodocumentação é melhor tanto para a capacidade de manutenção quanto para a legibilidade, independentemente do idioma.

A afirmação acima é densa, mas "auto-documentada":

double d = sqrt(square(x1 - x0) + square(y1 - y0));

Quando dividido em etapas (mais fácil para testes, com certeza), perde todo o contexto, como afirmado acima:

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

E, obviamente, usar nomes de variáveis e funções que declarem claramente seu propósito é inestimável.

Mesmo os blocos "if" podem ser bons ou ruins na autodocumentação. Isso é ruim porque você não pode forçar facilmente as duas primeiras condições a testar a terceira ... todas não estão relacionadas:

if (Bill is the boss) && (i == 3) && (the carnival is next weekend)

Este faz mais sentido "coletivo" e é mais fácil criar condições de teste:

if (iRowCount == 2) || (iRowCount == 50) || (iRowCount > 100)

E essa afirmação é apenas uma sequência aleatória de caracteres, vista de uma perspectiva de autodocumentação:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

Olhando para a declaração acima, manutenibilidade ainda é um grande desafio se as funções H1 e H2 alterarem as mesmas "variáveis de estado do sistema" em vez de serem unificadas em uma única função "H", porque alguém eventualmente alterará H1 sem sequer pensar existe uma função H2 para olhar e pode quebrar o H2.

Acredito que um bom design de código é muito desafiador porque não há regras rígidas que possam ser sistematicamente detectadas e aplicadas.

    
por 27.02.2018 / 10:41
fonte