One-liners vs. legibilidade: quando parar de reduzir o código? [fechadas]


14

Contexto

Recentemente, fiquei interessado em produzir um código melhor formatado. E por melhor, quero dizer "seguir regras endossadas por pessoas suficientes para considerá-la uma boa prática" (já que nunca haverá uma "melhor" maneira única de codificar, é claro).

Hoje em dia, eu principalmente codigo em Ruby, então comecei a usar um linter (Rubocop) para fornecer algumas informações sobre a "qualidade" do meu código (essa "qualidade" sendo definida pelo projeto orientado à comunidade do ruby-style-guide ).

Observe que usarei "qualidade" e "qualidade da formatação", não tanto sobre a eficiência do código, mesmo que em alguns casos, a eficiência do código seja afetada pela maneira como o código é escrito.

Enfim, fazendo tudo isso, percebi (ou pelo menos lembrei) algumas coisas:

  • Algumas linguagens (principalmente Python, Ruby e outras) permitem criar ótimas linhas de código
  • Seguir algumas diretrizes para seu código pode torná-lo significativamente mais curto e ainda assim muito claro
  • No entanto, seguir essas diretrizes muito estritamente pode tornar o código menos claro / fácil de ler
  • O código pode respeitar algumas diretrizes quase perfeitamente e ainda ser de baixa qualidade
  • A legibilidade do código é principalmente subjetiva (como em "o que eu acho claro pode ser completamente obscuro para um colega desenvolvedor")

Essas são apenas observações, não regras absolutas, é claro. Você também observará que a legibilidade do código e as diretrizes a seguir podem parecer não relacionadas neste momento, mas aqui as diretrizes são uma maneira de diminuir o número de maneiras de reescrever um pedaço de código.

Agora, alguns exemplos, para deixar tudo isso mais claro.

Exemplos

Vamos dar um caso de uso simples: temos uma aplicação com um " User" modelo. Um usuário tem um endereço opcional firstnamee surnameobrigatório email.

Quero escrever um método " name" que retorne o nome ( firstname + surname) do usuário, se pelo menos dele firstnameou surnameestiver presente, ou se emailfor um valor de fallback.

Eu também quero que este método use_emailuse " " como parâmetro (booleano), permitindo usar o email do usuário como o valor de fallback. Este use_emailparâmetro " " deve ser padronizado (se não for passado) como " true".

A maneira mais simples de escrever isso, em Ruby, seria:

def name(use_email = true)
 # If firstname and surname are both blank (empty string or undefined)
 # and we can use the email...
 if (firstname.blank? && surname.blank?) && use_email
  # ... then, return the email
  return email
 else
  # ... else, concatenate the firstname and surname...
  name = "#{firstname} #{surname}"
  # ... and return the result striped from leading and trailing spaces
  return name.strip
 end
end

Esse código é a maneira mais simples e fácil de entender. Mesmo para alguém que não "fala" Ruby.

Agora vamos tentar fazer isso mais curto:

def name(use_email = true)
 # 'if' condition is used as a guard clause instead of a conditional block
 return email if (firstname.blank? && surname.blank?) && use_email
 # Use of 'return' makes 'else' useless anyway
 name = "#{firstname} #{surname}"
 return name.strip
end

Isso é mais curto, ainda fácil de entender, se não mais fácil (a cláusula de guarda é mais natural de se ler do que um bloco condicional). A cláusula de guarda também a torna mais compatível com as diretrizes que eu estou usando, portanto, ganha-ganha aqui. Também reduzimos o nível de recuo.

Agora vamos usar um pouco de magia Ruby para torná-lo ainda mais curto:

def name(use_email = true)
 return email if (firstname.blank? && surname.blank?) && use_email
 # Ruby can return the last called value, making 'return' useless
 # and we can apply strip directly to our string, no need to store it
 "#{firstname} #{surname}".strip
end

Ainda mais curto e seguindo perfeitamente as diretrizes ... mas muito menos claro, pois a declaração de falta de retorno torna um pouco confuso para quem não está familiarizado com essa prática.

É aqui que podemos começar a fazer a pergunta: vale a pena? Deveríamos dizer "não, torne legível e adicione ' return'" (sabendo que isso não respeitará as diretrizes). Ou deveríamos dizer "Está tudo bem, é o jeito Ruby, aprenda a língua!"?

Se escolhermos a opção B, por que não torná-la ainda mais curta:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end

Aqui está, o one-liner! É claro que é mais curto ... aqui aproveitamos o fato de que Ruby retornará um valor ou outro dependendo de qual deles estiver definido (já que o email será definido nas mesmas condições de antes).

Também podemos escrever:

def name(use_email = true)
 (email if [firstname, surname].all?(&:blank?) && use_email) || "#{firstname} #{surname}".strip
end

É curto, não é tão difícil de ler (quero dizer, todos nós vimos como pode ser uma frase feia), bom Ruby, está em conformidade com as diretrizes que eu uso ... Mas ainda assim, comparado à primeira maneira de escrever é muito menos fácil de ler e entender. Também podemos argumentar que essa linha é muito longa (mais de 80 caracteres).

Questão

Alguns exemplos de código podem mostrar que escolher entre um código "em tamanho real" e muitas de suas versões reduzidas (até o famoso one-liner) pode ser difícil, pois, como podemos ver, os one-liners podem não ser tão assustadores, mas ainda assim, nada superará o código "em tamanho real" em termos de legibilidade ...

Então aqui está a verdadeira questão: onde parar? Quando é curto, curto o suficiente? Como saber quando o código se torna "muito curto" e menos legível (tendo em mente que é bastante subjetivo)? E ainda mais: como sempre codificar de acordo e evitar misturar one-liners com blocos de código "em tamanho normal" quando me apetece?

TL; DR

A principal questão aqui é: quando se trata de escolher entre um "longo, mas claro, legível e compreensível pedaço de código" e um "poderoso, mais curto e ainda mais difícil de ler / entender uma linha", sabendo que esses dois são os principais e os parte inferior de uma escala e não as duas únicas opções: como definir onde está a fronteira entre "suficientemente claro" e "não tão claro quanto deveria ser"?

A principal questão não é a clássica "one-liners vs. legibilidade: qual é o melhor?" mas "Como encontrar o equilíbrio entre os dois?"

Editar 1

Os comentários nos exemplos de código devem ser "ignorados", eles estão aqui para esclarecer o que está acontecendo, mas não devem ser levados em consideração ao avaliar a legibilidade do código.


7
Muito curto para uma resposta: mantenha a refatoração iterativa até que você não tenha certeza de que é melhor que a iteração anterior, pare e inverta a última refatoração.
Dom

8
Prefiro a variante 3 com a returnpalavra - chave adicionada . Esses sete personagens adicionam bastante clareza aos meus olhos.
cmaster - reinstate monica,

2
Se você está se sentindo realmente horrível, você pode escrever a coisa toda como [firstname,surname,!use_email].all?(&:blank?) ? email : "#{firstname} #{surname}".strip... porque false.blank?retorna verdadeiro e o operador ternário poupa alguns personagens ... ¯ \ _ (ツ) _ / ¯
DaveMongoose

1
OK, tenho que perguntar: que clareza a returnpalavra - chave deve adicionar ?! Não fornece nenhuma informação . É pura desordem.
21918 Konrad Rudolph

2
A noção de que brevidade gera clareza não apenas sofre com a lei dos retornos decrescentes, mas reverte quando levada ao extremo. Se você está reescrevendo para reduzir uma função curta, está perdendo tempo e o mesmo vale para tentar justificar a prática.
sdenham

Respostas:


26

Independentemente do código que você escreve, o melhor é legível. Curto é o segundo melhor. E legível geralmente significa curto o suficiente para que você possa entender o código, identificadores bem nomeados e aderir às expressões comuns do idioma em que o código está escrito.

Se isso fosse independente da linguagem, acho que isso definitivamente seria baseado em opiniões, mas dentro dos limites da linguagem Ruby, acho que podemos responder.

Primeiro, um recurso e uma maneira idiomática de escrever Ruby é omitir a returnpalavra - chave ao retornar um valor, a menos que retorne cedo de um método.

Outro recurso e idioma combinados é o uso de ifinstruções finais para aumentar a legibilidade do código. Uma das idéias principais do Ruby é escrever um código que seja lido como linguagem natural. Para isso, vamos ao Por que o Guia Pungente de Ruby, Capítulo 3 .

Leia o seguinte em voz alta para si mesmo.

5.times { print "Odelay!" }

Nas frases em inglês, a pontuação (como pontos, exclamações, parênteses) é silenciosa. A pontuação acrescenta significado às palavras, ajuda a fornecer pistas sobre o que o autor pretendia com uma frase. Então, vamos ler o texto acima como: Cinco vezes imprima “Odelay!”.

Diante disso, o exemplo de código nº 3 é mais idiomático para Ruby:

def name(use_email = true)
  return email if firstname.blank? && surname.blank? && use_email

  "#{firstname} #{surname}".strip
end

Agora, quando lemos o código, ele diz:

Retorne o e-mail se o primeiro nome estiver em branco e o sobrenome estiver em branco e use o e-mail

(retorno) nome e sobrenome removidos

O que é bem parecido com o código Ruby real.

São apenas duas linhas de código real, portanto, são bastante concisas e aderem aos idiomas da linguagem.


Bom ponto. É verdade que a pergunta não era para ser centrada em Ruby, mas concordo que não é possível ter uma resposta independente de idioma aqui.
Sudiukil 28/02

8
Acho que a ideia de fazer o código parecer uma linguagem natural é superestimada (e às vezes até problemática). Mas mesmo sem essa motivação, chego à mesma conclusão que esta resposta.
21918 Konrad Rudolph

1
Há mais um ajuste que eu consideraria fazer no código. Ou seja, colocar use_emailantes das outras condições, já que é uma variável e não uma chamada de função. Mas, novamente, a interpolação de cordas inunda a diferença de qualquer maneira.
John Dvorak

Estruturar código seguindo estruturas de linguagem natural pode fazer você cair nas armadilhas de linguagem. Por exemplo, quando você lê os próximos requisitos do send an email if A, B, C but no D, seguir sua premissa seria natural digitar 2 blocos if / else , quando provavelmente seria mais fácil codificar if not D, send an email. Tenha cuidado no momento de ler a linguagem natural e transforme-a em código, pois isso pode fazer você escrever uma nova versão da "História sem fim" . Com classes, métodos e variáveis. Afinal, não é grande coisa.
LAIV

@Laiv: tornar o código lido como linguagem natural não significa traduzir literalmente os requisitos. Significa escrever código para que, quando lido em voz alta, permita ao leitor entender a lógica sem ler todos os bits de código, caractere por caractere, construção de linguagem para construção de linguagem. Se a codificação if !Dfor melhor, tudo bem, lond, pois Dtem um nome significativo. E se o !operador se perder entre o outro código, ter um identificador chamado NotDseria apropriado.
Greg Burghardt

15

Eu não acho que você terá uma resposta melhor do que "use seu bom senso". Em suma, você deve buscar a clareza e não a falta . Muitas vezes, o código mais curto também é o mais claro, mas se você se concentrar apenas em obter clareza de falta, pode sofrer. Esse é claramente o caso dos dois últimos exemplos, o que requer mais esforço para entender do que os três exemplos anteriores.

Uma consideração importante é a audiência do código. A legibilidade é obviamente totalmente dependente da pessoa que está lendo. As pessoas que você espera ler o código (fora de si) realmente conhecem os idiomas da linguagem Ruby? Bem, essa pergunta não é algo que pessoas aleatórias na internet possam responder, essa é apenas sua própria decisão.


Eu concordo com o ponto de audiência, mas faz parte da minha luta: como meu software geralmente é de código aberto, o público pode ser composto por iniciantes e também por "deuses Ruby". Eu poderia simplificar para torná-lo acessível para a maioria das pessoas, mas parece um desperdício das vantagens oferecidas pelo idioma.
Sudiukil 28/02

1
Como alguém que teve que assumir, estender e manter um código realmente horrível, a clareza precisa vencer. Lembre-se do velho ditado: escreva seu código como se o mantenedor fosse um anjo do inferno vingativo que sabe onde você mora e onde seus filhos vão à escola.
uɐɪ

2
@Sudiukil: Esse é um ponto importante. Eu sugiro que você se esforce para obter código idiomático nesse caso (ou seja, assuma um bom conhecimento do idioma), pois é improvável que iniciantes contribuam para o código-fonte aberto de qualquer maneira. (Ou se o fizerem, eles vão estar preparado para colocar no esforço para aprender a língua.)
JacquesB

7

Parte do problema aqui é "o que é legibilidade". Para mim, eu olho para o seu primeiro exemplo de código:

def name(use_email = true)
 # If firstname and surname are both blank (empty string or undefined)
 # and we can use the email...
 if (firstname.blank? && surname.blank?) && use_email
  # ... then, return the email
  return email
 else
  # ... else, concatenate the firstname and surname...
  name = "#{firstname} #{surname}"
  # ... and return the result striped from leading and trailing spaces
  return name.strip
 end
end

E acho difícil ler, pois está cheio de comentários "barulhentos" que apenas repetem o código. Retire-os:

def name(use_email = true)
 if (firstname.blank? && surname.blank?) && use_email
  return email
 else
  name = "#{firstname} #{surname}"
  return name.strip
 end
end

e agora é muito mais legível. Ao lê-lo, penso "hmm, gostaria de saber se Ruby suporta o operador ternário? Em C #, posso escrever como:

string Name(bool useEmail = true) => 
    firstName.Blank() && surname.Blank() && useEmail 
    ? email 
    : $"{firstname} {surname}".Strip();

É possível algo assim em rubi? Analisando sua postagem, vejo que existe:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end

Tudo de bom. Mas isso não é legível para mim; simplesmente porque eu tenho que rolar para ver a linha inteira. Então, vamos consertar isso:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) 
 || "#{firstname} #{surname}".strip
end

Agora estou feliz. Não tenho muita certeza de como a sintaxe funciona, mas posso entender o que o código faz.

Mas sou só eu. Outras pessoas têm idéias muito diferentes sobre o que torna um bom código de leitura. Então, você precisa conhecer seu público ao escrever código. Se você é um iniciante em ensino absoluto, convém mantê-lo simples e possivelmente escrever como seu primeiro exemplo. Se você trabalha com um conjunto de desenvolvedores profissionais com muitos anos de experiência em ruby, escreva um código que tire proveito do idioma e mantenha-o curto. Se estiver em algum lugar no meio, mire em algum lugar no meio.

Uma coisa que eu diria: cuidado com o "código inteligente", como no seu último exemplo. Pergunte a si mesmo: [firstname, surname].all?(&:blank?)acrescenta algo além de fazer você se sentir inteligente, porque mostra suas habilidades, mesmo que agora seja um pouco mais difícil de ler? Eu diria que este exemplo provavelmente se encaixa nessa categoria. Se você estivesse comparando cinco valores, eu o veria como um bom código. Então, novamente, não há uma linha absoluta aqui, apenas lembre-se de ser inteligente demais.

Então, em resumo: a legibilidade requer que você conheça seu público-alvo e direcione seu código adequadamente e escreva um código sucinto, mas claro; nunca escreva código "inteligente". Mantenha-o curto, mas não muito curto.


2
Bem, eu esqueci de mencionar, mas os comentários foram feitos para serem "ignorados", eles estão aqui apenas para ajudar aqueles que não conhecem bem o Ruby. Ponto válido, porém, sobre o público, eu não pensei nisso. Quanto à versão que o deixa feliz: se é o comprimento da linha que importa, a terceira versão do meu código (aquela com apenas uma declaração de retorno) meio que faz isso e é ainda mais compreensível, não?
Sudiukil 28/02

1
@Sudiukil, por não ser um desenvolvedor de ruby, achei o mais difícil de ler e não se encaixava no que estava procurando (do ponto de vista de outro idioma) como a "melhor" solução. No entanto, para alguém familiarizado com o fato de o ruby ​​ser uma daquelas linguagens que retorna o valor da última expressão, provavelmente representa a versão mais simples e fácil de ler. Novamente, é tudo sobre o seu público.
David Arno

Não é um desenvolvedor Ruby, mas isso faz muito mais sentido para mim do que a resposta mais votada, que diz: "Aqui está o que vou retornar [nota de rodapé: sob uma condição longa específica]. Além disso, aqui está uma string que chegou atrasada para a festa." A lógica que é essencialmente apenas uma declaração de caso deve ser escrita como uma única declaração de caso uniforme, não espalhada por várias declarações aparentemente não relacionadas.
Paul

Pessoalmente, eu iria com o seu segundo bloco de código, exceto que eu combinaria as duas instruções no seu ramo else em uma:return "#{firstname} #{surname}".strip
Paul

2

Provavelmente, essa é uma pergunta em que é difícil não dar uma resposta com base em opiniões, mas aqui estão meus dois centavos.

Se você achar que tornar o código mais curto não afeta a legibilidade ou até a melhora, tente. Se o código ficar menos legível, será necessário considerar se existe um motivo razoavelmente bom para deixá-lo assim. Fazer isso apenas porque é mais curto, legal, ou apenas porque você pode, são exemplos de más razões. Você também precisa considerar se a redução do código tornaria menos compreensível para outras pessoas com quem você trabalha.

Então, qual seria uma boa razão? É uma decisão de julgamento, na verdade, mas um exemplo pode ser algo como uma otimização de desempenho (após testes de desempenho, é claro, sem antecedência). Algo que lhe proporciona algum benefício que você está disposto a pagar com menor legibilidade. Nesse caso, você pode atenuar a desvantagem fornecendo um comentário útil (que explica o que o código faz e por que ele precisava ser um pouco enigmático). Melhor ainda, você pode extrair esse código em uma função separada com um nome significativo, para que seja apenas uma linha no site de chamada que explique o que está acontecendo (por meio do nome da função) sem entrar em detalhes (no entanto, as pessoas têm diferenças) opiniões sobre isso, então essa é outra decisão que você deve fazer).


1

A resposta é um pouco subjetiva, mas você deve se perguntar com toda a honestidade que conseguir, se conseguir entender esse código quando voltar a usá-lo em um mês ou dois.

Cada mudança deve melhorar a capacidade da pessoa média de entender o código. Para tornar o código compreensível, é útil usar as seguintes diretrizes:

  • Respeite as expressões idiomáticas da linguagem . C #, Java, Ruby, Python têm suas formas preferidas de fazer a mesma coisa. As construções linguísticas ajudam a entender o código com o qual você não está familiarizado.
  • Pare quando seu código se tornar menos legível . No exemplo que você forneceu, isso aconteceu quando você atingiu seus últimos pares de códigos redutores. Você perdeu a vantagem idiomática do exemplo anterior e introduziu muitos símbolos que exigem muito pensamento para entender realmente o que está acontecendo.
  • Use apenas comentários quando tiver que justificar algo inesperado . Eu sei que seus exemplos estavam lá para explicar construções para pessoas menos familiarizadas com Ruby, e isso é bom para uma pergunta. Prefiro usar comentários para explicar regras comerciais inesperadas e evitá-las se o código puder falar por si.

Dito isto, há momentos em que o código expandido ajuda a entender o que está acontecendo melhor. Um exemplo disso vem do C # e do LINQ. O LINQ é uma ótima ferramenta e pode melhorar a legibilidade em algumas situações, mas também encontrei várias situações em que era muito mais confuso. Eu tive alguns comentários na revisão por pares que sugeriram transformar a expressão em um loop com instruções if apropriadas para que outras pessoas pudessem mantê-la melhor. Quando eu cumpri, eles estavam certos. Tecnicamente, o LINQ é mais idiomático para C #, mas há casos em que diminui a compreensibilidade e uma solução mais detalhada a aprimora.

Eu digo tudo isso para dizer o seguinte:

Melhore quando você pode melhorar seu código (mais compreensível)

Lembre-se de que você ou alguém como você precisará manter esse código posteriormente. A próxima vez que você se deparar com isso poderá demorar meses. Faça um favor a si mesmo e não procure reduzir a contagem de linhas com o custo de entender seu código.


0

A legibilidade é uma propriedade que você deseja ter, ter muitos liners não. Então, em vez de "one-liners vs readability", a pergunta deve ser:

Quando as one-liners aumentam a legibilidade e quando elas prejudicam?

Eu acredito que one-liners são bons para facilitar a leitura quando cumprem essas duas condições:

  1. Eles são muito específicos para serem extraídos para uma função.
  2. Você não deseja interromper o "fluxo" da leitura do código ao redor.

Por exemplo, digamos que namenão era um bom nome para o seu método. Combinar o primeiro nome e o sobrenome, ou usar o email em vez do nome, não era algo natural a se fazer. Então, ao invés dename melhor coisa que você pode encontrar, acabou sendo complicado e longo:

puts "Name: #{user.email_if_there_is_no_name_otherwise_use_firstname_and_surname(use_email)}"

Esse nome longo indica que isso é muito específico - se fosse mais geral, você poderia ter encontrado um nome mais geral. Portanto, agrupá-lo em um método não ajuda na legibilidade (é muito longa) nem no DRYness (muito específico para ser usado em qualquer outro lugar); portanto, é melhor deixar o código lá.

Ainda - por que torná-lo um one-liner? Eles geralmente são menos legíveis que o código de múltiplas linhas. É aqui que devemos verificar minha segunda condição - o fluxo do código circundante. E se você tiver algo parecido com isto:

puts "Group: #{user.group}"
puts "Title: #{user.title}"
if user.firstname.blank? && user.surname.blank?) && use_email
  name = email
else
  name = "#{firstname} #{surname}"
  name.strip
end
puts "Name: #{name}"
puts "Age: #{user.age}"
puts "Address: #{user.address}"

O próprio código multilinha é legível - mas quando você tenta ler o código circundante (imprimindo os vários campos), essa construção multilinha está interrompendo o fluxo. É mais fácil de ler:

puts "Group: #{user.group}"
puts "Title: #{user.title}"
puts "Name: #{(email if (user.firstname.blank? && user.surname.blank?) && use_email) || "#{user.firstname} #{user.surname}".strip}"
puts "Age: #{user.age}"
puts "Address: #{user.address}"

Seu fluxo não é interrompido e você pode se concentrar na expressão específica, se precisar.

Esse é o seu caso? Definitivamente não!

A primeira condição é menos relevante - você já considerou geral o suficiente para merecer um método e criou um nome para esse método que é muito mais legível do que sua implementação. Obviamente, você não a extrairia para uma função novamente.

Quanto à segunda condição - interrompe o fluxo do código circundante? Não! O código circundante é uma declaração de método, que escolher nameé seu único objetivo. A lógica de escolher o nome não está interrompendo o fluxo do código circundante - é o próprio objetivo do código circundante!

Conclusão - não faça de todo o corpo da função uma linha

As one-liners são boas quando você quer fazer algo um pouco complexo sem interromper o fluxo. Uma declaração de função já está interrompendo o fluxo (para que não seja interrompida quando você chama essa função), portanto, tornar todo o corpo da função uma linha única não ajuda na legibilidade.

Nota

Estou me referindo a funções e métodos "completos" - não a funções embutidas ou expressões lambda que geralmente fazem parte do código circundante e precisam se encaixar no fluxo.

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.