Legibilidade versus capacidade de manutenção, caso especial de gravação 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);

Recentemente, mudei para um departamento em que o seguinte estilo de codificação é muito utilizado:

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

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

Receio que, no caso de uma falha devido ao mesmo problema programado na primeira maneira, não seja capaz de saber qual função causou a falha.

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

Meu medo está correto ou estou faltando alguma coisa e, em geral, a preferida em um ambiente comercial? Legibilidade ou manutenção?

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


17
@gnat: você se refere a uma pergunta geral, enquanto eu estou especialmente interessado no caso mencionado de chamadas de funções aninhadas e as conseqüências em caso de análise de despejo de memória, mas obrigado pelo link, ele contém algumas informações interessantes.
Dominique

9
Observe que, se esse exemplo for aplicado ao C ++ (como você mencionou que está sendo usado no seu projeto), isso não é apenas uma questão de estilo, pois a ordem de avaliação das invocações HXe GXpode mudar na linha única, como a ordem de avaliação dos argumentos da função não é especificada. Se, por algum motivo, você depender da ordem dos efeitos colaterais (consciente ou inconscientemente) nas invocações, essa "refatoração de estilo" poderá acabar afetando mais do que apenas legibilidade / manutenção.
dfri 22/02

4
É o nome da variável result_g1que você realmente usa ou esse valor realmente representa algo com um nome sensível; por exemplo percentageIncreasePerSecond. Isso seria realmente o meu teste para decidir entre os dois
Richard Tingle

3
Independentemente de seus sentimentos sobre o estilo de codificação, você deve seguir a convenção que já está em vigor, a menos que esteja claramente errado (não parece que esteja nesse caso).
N00b 22/02

4
@ t3chb0t Você é livre para votar da maneira que quiser, mas esteja ciente de que é bom incentivar perguntas boas, úteis e sobre tópicos neste site (e desencorajar as ruins), de que o objetivo de votar para cima ou para baixo em uma pergunta é para indicar se uma pergunta é útil e clara, portanto, votar por outros motivos, como usar uma votação como um meio de criticar algum código de exemplo postado para ajudar o contexto da pergunta geralmente não é útil para manter a qualidade do site : softwareengineering.stackexchange.com/help/privileges/vote-down
Ben Cottrell

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 destacam tudo de uma só vez
  3. É livre de nomes descritivos

Se você expandi-lo com resultados intermediários, 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 destacam tudo de uma só vez
  3. É livre de nomes descritivos
  4. Está cheio de nomes não descritivos

Se você expandi-lo com nomes que adicionam significado novo, bom e 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. Isso corrige os problemas e é claramente melhor do que qualquer outra coisa oferecida aqui, mas exige que você crie os nomes.

Se você faz isso com nomes sem sentido como result_thise result_thatporque simplesmente não consegue pensar em bons nomes, eu realmente prefiro que você nos poupe da confusão de nomes sem sentido e a expanda usando um bom e velho espaço em branco:

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

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

  1. É denso
  2. Alguns depuradores destacam tudo de uma só vez
  3. É livre de nomes descritivos
  4. Está cheio de nomes não descritivos

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

Por alguma razão, os depuradores adoram novas linhas, então você deve achar que isso não é difícil para depurar:

insira a descrição da imagem aqui

Se isso não for suficiente, imagine que G2()foi chamado em mais de um lugar e aconteceu:

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

Eu acho bom que, uma vez que cada G2()chamada esteja em sua própria linha, esse estilo o leve diretamente à chamada ofensiva em geral.

Portanto, não use os problemas 1 e 2 como uma desculpa para nos manter com o problema 4. Use bons nomes quando puder pensar neles. Evite nomes sem sentido quando não puder.

As Raças da Luminosidade no comentário da Orbit apontam corretamente que essas funções são artificiais e têm nomes próprios mortos. Então, aqui está um exemplo de aplicação desse estilo a algum código natural:

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 sob 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, eu descobri que esse estilo funciona bem com o código funcional que está sendo movido para o espaço orientado a objetos. Se você pode criar bons nomes para fazer isso em estilo intermediário, terá mais poder para você. Até então eu estou usando isso. Mas, em qualquer caso, por favor, encontre uma maneira de evitar nomes de resultados sem sentido. Eles fazem meus olhos doerem.


20
@ Steve e eu não estou dizendo para você não. Estou implorando por um nome significativo. Muitas vezes eu já vi o estilo intermediário feito sem pensar. Nomes ruins queimam meu cérebro muito mais do que o código esparso por linha. Não deixo considerações de largura ou comprimento me motivarem a tornar meu código denso ou meus nomes curtos. Deixei que eles me motivassem a decompor mais. Se bons nomes simplesmente não acontecerem, considere este trabalho para evitar ruídos sem sentido.
Candied_orange

6
Acrescento à sua postagem: tenho uma pequena regra geral: se você não pode nomear, pode ser um sinal de que não está bem definida. Eu o uso em entidades, propriedades, variáveis, módulos, menus, classes auxiliares, métodos etc. Em inúmeras situações, essa pequena regra revelou uma falha séria no design. Portanto, de certa forma, a boa nomeação não apenas contribui para a legibilidade e a manutenção, mas também ajuda a verificar o design. Claro que existem exceções para todas as regras simples.
Alireza

4
A versão expandida parece feia. Há muito espaço em branco lá, o que reduz os efeitos positivos, já que tudo é faseado com ele, significando nada.
Mateen Ulhaq

5
@MateenUlhaq O único espaço em branco extra que há algumas novas linhas e algum recuo, e tudo é cuidadosamente colocado em limites significativos . Seu comentário coloca espaços em branco em limites não significativos. Eu sugiro que você dê uma olhada um pouco mais perto e mais aberta.
Jpmc26

3
Ao contrário de @MateenUlhaq, estou em cima do espaço em branco neste exemplo em particular com esses nomes de funções, mas com nomes de funções reais (que têm mais de dois caracteres, certo?), Poderia ser o que eu procuraria.
Lightness Races com Monica

50

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

Eu discordo totalmente disso. Basta olhar para 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 da 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 diria 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 fácil de ler e entender, com um bom conjunto de testes automatizados.

Portanto, se você estiver transformando uma única expressão em uma linha múltipla 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ódigo que não precise depurar sobre código fácil de depurar.


37
Embora eu concorde que isso F(G1(H1(b1), H2(b2)), G2(c1))seja difícil de ler, isso não tem nada a ver com ficar muito denso. (Não tenho certeza se você quis dizer isso, mas pode ser interpretado dessa maneira.) Aninhar três ou quatro funções em uma única linha pode ser perfeitamente legível, principalmente se algumas das funções forem simples operadores de infix. São os nomes não-descritivos que são o problema aqui, mas esse problema é ainda pior na versão de várias linhas, onde mais nomes não-descritivos são introduzidos. Adicionar apenas clichês quase nunca ajuda na legibilidade.
leftaroundabout

23
@leftaroundabout: Para mim, a dificuldade é que não é óbvio se são G1necessários 3 parâmetros ou apenas 2 e G2é outro parâmetro F. Eu tenho que apertar os olhos e contar os parênteses.
Matthieu M.

4
@MatthieuM. isso pode ser um problema, embora, se as funções forem bem conhecidas, muitas vezes é óbvio o que leva quantos argumentos. Especificamente, como eu disse, para funções infix , é imediatamente claro que eles usam dois argumentos. (Além disso, a sintaxe parêntesis-tuplas maioria das linguagens de usar agrava este problema, em uma linguagem que prefere Currying é automaticamente mais clara: F (G1 (H1 b1) (H2 b2)) (G2 c1).)
leftaroundabout

5
Pessoalmente, prefiro a forma mais compacta, desde que exista um estilo ao redor, como no meu comentário anterior, porque garante menos estado para acompanhar mentalmente - result_h1não pode ser reutilizado se não existir, e o encanamento entre as 4 variáveis ​​é óbvio.
Izkata 22/02

8
Eu descobri que o código que é fácil de depurar geralmente é o código que não precisa de depuração.
Rob K

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 código verdadeiro pode ser bom nesse aspecto, não podemos dizer. De qualquer forma, é muito complicado devido à densidade de informações extremamente baixa, que geralmente não se presta a um entendimento fácil.

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

Depois de introduzir nomes significativos, você pode verificar se uma das formas parece natural ou se existe um meio de ouro para se escolher.

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


17

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

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

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


O ponto é que não há uma única complexidade mensurável de declaração 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));

Essa é uma afirmação bastante complexa, mas qualquer programador que se preze deve poder entender imediatamente o que isso faz. É 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 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));

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


14

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

É possível ter um código que possa ser mantido, mas ilegível? Por outro lado, se o código é extremamente legível, por que ele se tornaria insustentável por ser legível? Nunca ouvi falar de nenhum programador que jogasse esses fatores um contra o outro, tendo que escolher um ou outro!

Em termos de decisão de usar variáveis ​​intermediárias para chamadas de funções aninhadas, no caso de 3 variáveis ​​fornecidas, chamadas para 5 funções separadas e algumas chamadas aninhadas em 3 profundas, eu tenderia a usar pelo menos algumas variáveis ​​intermediárias para detalhar isso, como você fez.

Mas certamente não chego ao ponto de 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 têm influência 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 as 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. A mistura de funções de diferentes áreas em diferentes níveis de aninhamento é propensa a deixar o código parecido com a orelha de um porco.

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

  4. Um pequeno ninho de chamadas de função que executa uma funcionalidade simples e que já é clara de ler, e depois é decomposto excessivamente e atomizado, é capaz de ser mais difícil de ler do que aquele que não foi decomposto.


3
+1 a "É possível ter um código que possa ser mantido, mas ilegível?". Esse foi o meu primeiro pensamento também.
22418 RonJohn

4

Ambos são subótimos. Considere os 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 gerais:

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

Ao decidir quais resultados serão explicados, lembre-se do custo (cópia x referência, valor l vs valor r), legibilidade e risco, individualmente para cada declaração.

Por exemplo, não há valor agregado ao mover conversões simples de unidade / tipo para suas próprias linhas, porque 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);

Com relação à sua preocupação em analisar despejos de memória, a validação de entrada geralmente é muito mais importante - é provável que ocorra uma falha real nessas funções, e não na linha que as chama, e mesmo se não for, você normalmente não precisa saber exatamente onde as coisas explodiram. É muito mais importante saber onde as coisas começaram a desmoronar do que saber onde elas finalmente explodiram, que é o que a validação de entrada captura.


Quanto ao custo de aprovação de um argumento: existem duas regras de otimização. 1) Não. 2) (somente para especialistas) Ainda não .
RubberDuck

1

A legibilidade é a maior parte da manutenção. Duvida de mim? Escolha um projeto grande em uma linguagem que você não conhece (provavelmente a linguagem de programação e a linguagem dos programadores) e veja como você a refatoraria ...

Eu colocaria a legibilidade como algo entre 80 e 90 de manutenção. Os outros 10% a 20% são como favoráveis ​​à refatoração.

Dito isto, você efetivamente passa duas variáveis ​​para sua função final (F). Essas 2 variáveis ​​são criadas usando outras 3 variáveis. Seria melhor passar b1, b2 e c1 para F, se F já existir, crie D que faça a composição para F e retorne 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 está incorreto, a métrica não é a página, é o método, e a lógica MENOS 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 um programador pode entender. Leia sobre complexidade ciclomática.


11
Eu concordo com tudo o que você diz sobre legibilidade. Mas eu não concordo que quebra uma operação lógica em métodos distintos, necessariamente torna mais legível do que quebrá-lo em linhas separadas (ambas as técnicas que pode , quando usado em demasia, fazem lógica simples menos legível, e fazer todo o programa mais confuso) - se você quebrar demais as coisas nos métodos, acaba imitando macros da linguagem assembly e perde de vista como elas se integram como um todo. Além disso, nesse método separado, você ainda enfrentaria o mesmo problema: aninhe as chamadas ou divida-as em variáveis ​​intermediárias.
23418 Steve Steve

@ Steve: Eu não disse que sempre faça isso, mas se você estiver pensando em usar 5 linhas para obter um único valor, há uma boa chance de que uma função seja melhor. Quanto às linhas múltiplas versus linha complexa: se for uma função com um bom nome, ambas funcionarão igualmente bem.
jmoreno

1

Independentemente de você estar em C # ou C ++, desde que esteja em uma compilação de depuração, uma solução possível é agrupar 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 para onde o problema está simplesmente observando o rastreamento da pilha.

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

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

  • Olhando para 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 2 chamadas idênticas etc.

Esta não é uma bala de prata, mas não é tão ruim assim.

Sem mencionar que o agrupamento de funções do grupo pode ser ainda 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));

1

Na minha opinião, o código de auto-documentação é melhor para manutenção e legibilidade, independentemente do idioma.

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

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

Quando dividido em estágios (mais fácil para testar, certamente) perde todo o contexto, conforme declarado 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, o uso de nomes de variáveis ​​e funções que afirmam claramente seu objetivo é inestimável.

Mesmo blocos "se" podem ser bons ou ruins na auto-documentação. Isso é ruim porque você não pode forçar facilmente as duas primeiras condições a testar a terceira ... todas não têm relação:

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 auto-documentação:

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

Observando a afirmação acima, a manutenção ainda é um grande desafio se as funções H1 e H2 alteram as mesmas "variáveis ​​de estado do sistema" em vez de serem unificadas em uma única função "H", porque alguém eventualmente altera o H1 sem nem pensar que há uma Função H2 para olhar e pode quebrar 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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.