One-liners vs. legibilidad: ¿cuándo dejar de reducir código? [cerrado]


14

Contexto

Recientemente me interesé en producir un código mejor formateado. Y por mejor quiero decir "seguir las reglas respaldadas por suficientes personas para considerarlo una buena práctica" (ya que, por supuesto, nunca habrá una "mejor" forma única de codificar).

En estos días, codifico principalmente en Ruby, así que comencé a usar un linter (Rubocop) para proporcionarme información sobre la "calidad" de mi código (esta "calidad" está definida por la guía de estilo ruby ​​del proyecto impulsado por la comunidad ).

Tenga en cuenta que usaré "calidad" como en "calidad del formateo", no tanto sobre la eficiencia del código, incluso si en algunos casos, la eficiencia del código en realidad se ve afectada por la forma en que se escribe el código.

De todos modos, al hacer todo eso, me di cuenta (o al menos recordé) algunas cosas:

  • Algunos lenguajes (más notablemente Python, Ruby y otros) permiten crear código de líneas geniales.
  • Seguir algunas pautas para su código puede hacerlo significativamente más corto y aún así muy claro
  • Sin embargo, seguir estas pautas demasiado estrictamente puede hacer que el código sea menos claro / fácil de leer.
  • El código puede respetar algunas pautas casi a la perfección y aún ser de baja calidad.
  • La legibilidad del código es principalmente subjetiva (como en "lo que encuentro claro podría ser completamente oscuro para un desarrollador desarrollador")

Esas son solo observaciones, no reglas absolutas, por supuesto. También notará que la legibilidad del código y las siguientes pautas pueden parecer ajenas en este punto, pero aquí las pautas son una forma de reducir la cantidad de formas de reescribir un fragmento de código.

Ahora, algunos ejemplos, para aclarar todo eso.

Ejemplos

Tomemos un caso de uso simple: tenemos una aplicación con un " User" modelo. Un usuario tiene una dirección opcional firstnamey surnameobligatoria email.

Quiero escribir un método " name" que devolverá entonces el nombre ( firstname + surname) del usuario si al menos su firstnameo surnameestá presente, o su emailcomo un valor de retorno en caso contrario.

También quiero que este método tome un " use_email" como parámetro (booleano), lo que permite utilizar el correo electrónico del usuario como valor de reserva. Este " use_email" parámetro debe ser predeterminado (si no se pasa) como " true".

La forma más simple de escribir eso, en Ruby, sería:

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

Este código es la forma más simple y fácil de entender. Incluso para alguien que no "habla" Ruby.

Ahora intentemos hacerlo más corto:

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

Esto es más corto, aún fácil de entender, si no más fácil (la cláusula de protección es más natural de leer que un bloque condicional). La cláusula de protección también lo hace más compatible con las pautas que estoy usando, así que gane-gane aquí. También reducimos el nivel de sangría.

Ahora usemos algo de magia Ruby para hacerlo aún más corto:

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

Incluso más corto y siguiendo las pautas perfectamente ... pero mucho menos claro ya que la falta de declaración de devolución hace que sea un poco confuso para aquellos que no están familiarizados con esta práctica.

Es aquí donde podemos comenzar a hacer la pregunta: ¿realmente vale la pena? Deberíamos decir "no, hacerlo legible y agregar ' return'" (sabiendo que esto no respetará las pautas). ¿O deberíamos decir "Está bien, es la forma de Ruby, aprende el maldito idioma"?

Si tomamos la opción B, ¿por qué no hacerlo aún más corto?

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

Aquí está, el one-liner! Por supuesto, es más corto ... aquí aprovechamos el hecho de que Ruby devolverá un valor u otro dependiendo de cuál esté definido (ya que el correo electrónico se definirá en las mismas condiciones que antes).

También podemos escribirlo:

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

Es breve, no es tan difícil de leer (quiero decir, todos hemos visto lo que puede parecer un trazador de líneas feo), bueno Ruby, cumple con la directriz que uso ... Pero aún así, en comparación con la primera forma de escribir es mucho menos fácil de leer y entender. También podemos argumentar que esta línea es demasiado larga (más de 80 caracteres).

Pregunta

Algunos ejemplos de código pueden mostrar que elegir entre un código de "tamaño completo" y muchas de sus versiones reducidas (hasta el famoso one-liner) puede ser difícil ya que, como podemos ver, los one-liners no pueden ser tan temibles, pero aún así, nada superará el código de "tamaño completo" en términos de legibilidad ...

Así que aquí está la verdadera pregunta: ¿dónde parar? ¿Cuándo es corto, lo suficientemente corto? ¿Cómo saber cuándo el código se vuelve "demasiado corto" y menos legible (teniendo en cuenta que es bastante subjetivo)? Y aún más: ¿cómo codificar siempre en consecuencia y evitar mezclar frases con trozos de código "de tamaño completo" cuando me da la gana?

TL; DR

La pregunta principal aquí es: cuando se trata de elegir entre un "fragmento de código largo pero claro, legible y comprensible" y un "poderoso, más corto pero más difícil de leer / entender", sabiendo que esos dos son los mejores y los mejores. fondo de una escala y no las dos únicas opciones: ¿cómo definir dónde está la frontera entre "lo suficientemente claro" y "no tan claro como debería ser"?

La pregunta principal no es la clásica "One-liners vs. legibilidad: ¿cuál es mejor?" pero "¿Cómo encontrar el equilibrio entre esos dos?"

Editar 1

Los comentarios en los ejemplos de código están destinados a ser "ignorados", están aquí para aclarar lo que está sucediendo, pero no deben tenerse en cuenta al evaluar la legibilidad del código.


77
Demasiado corto para una respuesta: siga refactorizando iterativamente hasta que no esté seguro de que es mejor que la iteración anterior, luego detenga y revierta la última refactorización.
Dom

8
Prefiero la variante 3 con la returnpalabra clave agregada . Esos siete personajes agregan bastante claridad en mis ojos.
cmaster - reinstalar a monica

2
Si te sientes realmente horrible, puedes escribir todo como [firstname,surname,!use_email].all?(&:blank?) ? email : "#{firstname} #{surname}".strip... porque false.blank?devuelve verdadero y el operador ternario te ahorra algunos caracteres ... ¯ \ _ (ツ) _ / ¯
DaveMongoose

1
OK, tengo que preguntar: ¿qué claridad se returnsupone que debe agregar la palabra clave? No proporciona información alguna . Es puro desorden.
Konrad Rudolph

2
La noción de que la brevedad engendra claridad no solo adolece de la ley de rendimientos decrecientes, sino que también se invierte cuando se la lleva al extremo. Si está reescribiendo para acortar una función corta, está desperdiciando su tiempo, y lo mismo ocurre con el intento de justificar la práctica.
sdenham

Respuestas:


26

No importa qué código escriba, legible es lo mejor. Corto es el segundo mejor. Por lo general, legible significa lo suficientemente breve como para que pueda entender el código, los identificadores bien nombrados y adherirse a los modismos comunes del idioma en el que está escrito el código.

Si esto fuera un lenguaje agnóstico, creo que definitivamente estaría basado en una opinión, pero dentro de los límites del lenguaje Ruby, creo que podemos responderlo.

Primero, una característica y una forma idiomática de escribir Ruby es omitir la returnpalabra clave cuando devuelve un valor, a menos que regrese temprano de un método.

Otra característica e idioma combinados es el uso de ifdeclaraciones finales para aumentar la legibilidad del código. Una de las ideas de manejo en Ruby es escribir código que se lea como lenguaje natural. Para esto, vamos a _why's Poignant Guide to Ruby, Chapter 3 .

Lea lo siguiente en voz alta para usted mismo.

5.times { print "Odelay!" }

En las oraciones en inglés, los signos de puntuación (como puntos, exclamaciones, paréntesis) son silenciosos. La puntuación agrega significado a las palabras, ayuda a dar pistas sobre lo que el autor pretendía con una oración. Así que leamos lo anterior como: Cinco veces imprime "¡Odelay!".

Dado esto, el ejemplo de código # 3 es más idiomático para Ruby:

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

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

Ahora, cuando leemos el código, dice:

Devuelva el correo electrónico si el nombre está en blanco y el apellido está en blanco y use el correo electrónico

(volver) nombre y apellido despojados

Lo cual está bastante cerca del código real de Ruby.

Es solo 2 líneas de código real, por lo que es bastante breve y se adhiere a los modismos del lenguaje.


Buen punto Es cierto que la pregunta no estaba centrada en Ruby, pero sí estoy de acuerdo en que no es posible tener una respuesta agnóstica del lenguaje aquí.
Sudiukil

8
Encuentro la idea de hacer que el código suene como lenguaje natural muy sobrevalorado (y a veces incluso problemático). Pero incluso sin esta motivación llego a la misma conclusión que esta respuesta.
Konrad Rudolph

1
Hay un ajuste más que consideraría hacerle al código. Es decir, anteponer use_emaillas otras condiciones ya que es una variable en lugar de una llamada a la función. Pero, de nuevo, la interpolación de cuerdas inunda la diferencia de todos modos.
John Dvorak

Estructurar el código siguiendo las estructuras del lenguaje natural puede hacer que caigas en las trampas del lenguaje. Por ejemplo, cuando lea los siguientes requisitos do send an email if A, B, C but no D, seguir su premisa sería natural escribir 2 bloques if / else , cuando probablemente sería más fácil de codificar if not D, send an email. Tenga cuidado al momento de leer el lenguaje natural y transformelo en código porque puede hacer que escriba una nueva versión de la "Historia interminable" . Con clases, métodos y variables. No es gran cosa después de todo.
Laiv

@Laiv: Hacer que el código se lea como lenguaje natural no significa literalmente traducir los requisitos. Significa escribir código para que cuando se lea en voz alta, permita al lector comprender la lógica sin leer cada bit de código, carácter por carácter, construcción del lenguaje por construcción del lenguaje. Si la codificación if !Des mejor, está bien que Londres Dtenga un nombre significativo. Y si el !operador se pierde entre el otro código, NotDsería apropiado tener un identificador llamado .
Greg Burghardt

15

No creo que obtenga una mejor respuesta que "use su mejor juicio". En resumen, debe luchar por la claridad en lugar de la brevedad . A menudo, el código más corto también es el más claro, pero si te enfocas solo en lograr la brevedad, la claridad puede verse afectada. Este es claramente el caso en los últimos dos ejemplos, que requiere más esfuerzo para comprender que los tres ejemplos anteriores.

Una consideración importante es la audiencia del código. La legibilidad, por supuesto, depende totalmente de la persona que lee. ¿Las personas que espera que lean el código (además de usted) realmente conocen los modismos del lenguaje Ruby? Bueno, esta pregunta no es algo que las personas al azar en Internet puedan responder, esta es solo su propia decisión.


Estoy de acuerdo con el punto de audiencia, pero es parte de mi lucha: como mi software a menudo es de código abierto, la audiencia podría estar compuesta por principiantes y por "dioses de Ruby". Podría hacerlo simple para que sea accesible para la mayoría de las personas, pero se siente como un desperdicio de las ventajas que ofrece el idioma.
Sudiukil

1
Como alguien que ha tenido que hacerse cargo, extender y mantener un código verdaderamente horrible, la claridad tiene que ganar. Recuerde el viejo adagio: escriba su código como si el responsable de mantenimiento fuera un vengativo Ángel del Infierno que sabe dónde vive y dónde van sus hijos a la escuela.
U

2
@Sudiukil: Ese es un punto importante. Le sugiero que se esfuerce por obtener un código idiomático en ese caso (es decir, suponga un buen conocimiento del idioma), ya que es poco probable que los principiantes contribuyan al código fuente abierto de todos modos. (O si lo hacen, estarán preparados para esforzarse por aprender el idioma).
JacquesB

7

Parte del problema aquí es "qué es la legibilidad". Para mí, miro tu primer ejemplo 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

Y me resulta difícil de leer ya que está lleno de comentarios "ruidosos" que simplemente repiten el código. Pelarlos:

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

y ahora es mucho más legible. Al leerlo, pienso "hmm, me pregunto si Ruby es compatible con el operador ternario. En C #, puedo escribirlo como:

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

¿Es posible algo así en Ruby? Trabajando en tu publicación, veo que hay:

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

Todas las cosas buenas Pero eso no es legible para mí; simplemente porque tengo que desplazarme para ver toda la línea. Así que arreglemos eso:

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

Ahora estoy feliz. No estoy completamente seguro de cómo funciona la sintaxis, pero puedo entender lo que hace el código.

Pero solo soy yo. Otras personas tienen ideas muy diferentes sobre lo que hace que sea un código agradable de leer. Por lo tanto, debe conocer a su audiencia al escribir código. Si eres un principiante absoluto, entonces querrás que sea simple y posiblemente escribirlo como tu primer ejemplo. Si trabaja entre un conjunto de desarrolladores profesionales con muchos años de experiencia en ruby, escriba código que aproveche el lenguaje y que sea breve. Si está en algún punto intermedio, entonces busca un punto intermedio.

Sin embargo, una cosa diría: tenga cuidado con el "código inteligente", como en su último ejemplo. Pregúntate a ti mismo, ¿ [firstname, surname].all?(&:blank?)agrega algo más que hacerte sentir inteligente porque muestra tus habilidades, incluso si ahora es un poco más difícil de leer? Yo diría que este ejemplo probablemente se incluye en esa categoría. Sin embargo, si comparara cinco valores, lo vería como un buen código. Entonces, de nuevo, no hay una línea absoluta aquí, solo ten en cuenta que eres demasiado inteligente.

En resumen: la legibilidad requiere que conozca a su audiencia y apunte su código en consecuencia y escriba un código breve pero claro; nunca escriba código "inteligente". Mantenlo corto, pero no demasiado corto.


2
Bueno, olvidé mencionarlo, pero los comentarios estaban destinados a ser "ignorados", están aquí solo para ayudar a aquellos que no conocen bien a Ruby. Punto válido sobre la audiencia, no pensé en eso. En cuanto a la versión que te hace feliz: si lo que importa es la longitud de la línea, la tercera versión de mi código (la que tiene solo una declaración de retorno) hace eso y es incluso un poco más comprensible, ¿no?
Sudiukil

1
@Sudiukil, al no ser un desarrollador de ruby, me pareció la más difícil de leer y no encajaba con lo que estaba buscando (desde la perspectiva de otro idioma) como la "mejor" solución. Sin embargo, para alguien familiarizado con el hecho de que Ruby es uno de esos lenguajes que devuelve el valor de la última expresión, probablemente representa la versión más simple y fácil de leer. Nuevamente, se trata de tu audiencia.
David Arno

No soy un desarrollador de Ruby, pero esto tiene mucho más sentido para mí que la respuesta más votada, que dice "Esto es lo que voy a devolver [nota al pie: bajo una condición larga específica]. Además, aquí hay una cadena que llegó tarde a la fiesta." La lógica que es esencialmente una declaración de caso debe escribirse como una declaración de caso uniforme única, no distribuida en múltiples declaraciones aparentemente no relacionadas.
Paul

Personalmente, iría con su segundo bloque de código, excepto que combinaría las dos declaraciones en su rama else en una:return "#{firstname} #{surname}".strip
Paul

2

Esta es probablemente una pregunta en la que es difícil no dar una respuesta basada en una opinión, pero aquí están mis dos centavos.

Si encuentra que acortar el código no afecta la legibilidad, o incluso lo mejora, hágalo. Si el código se vuelve menos legible, entonces debe considerar si hay una razón bastante buena para dejarlo así. Hacerlo solo porque es más corto o genial, o simplemente porque puedes, son ejemplos de malas razones. También debe considerar si acortar el código lo haría menos comprensible para otras personas con las que trabaja.

Entonces, ¿cuál sería una buena razón? Realmente es una decisión, pero un ejemplo podría ser algo así como una optimización del rendimiento (después de las pruebas de rendimiento, por supuesto, no de antemano). Algo que le brinda algún beneficio que está dispuesto a pagar con una menor legibilidad. En ese caso, puede mitigar el inconveniente proporcionando un comentario útil (que explique qué hace el código y por qué tuvo que ser un poco críptico). Aún mejor, puede extraer ese código en una función separada con un nombre significativo, de modo que sea solo una línea en el sitio de la llamada que explica lo que está sucediendo (a través del nombre de la función) sin entrar en detalles (sin embargo, las personas tienen diferencias opiniones sobre esto, por lo que esta es otra decisión que debe hacer).


1

La respuesta es un poco subjetiva, pero tienes que preguntarte con toda la honestidad que puedas reunir, si pudieras entender ese código cuando regreses a él en un mes o dos.

Cada cambio debería mejorar la capacidad de la persona promedio para comprender el código. Para que el código sea comprensible, es útil usar las siguientes pautas:

  • Respeta los modismos del lenguaje . C #, Java, Ruby, Python tienen sus formas preferidas de hacer lo mismo. Las construcciones idiomáticas ayudan a comprender el código con el que no está familiarizado.
  • Deténgase cuando su código sea menos legible . En el ejemplo que proporcionó, eso sucedió cuando golpeó sus últimos pares de código reductor. Perdió la ventaja idiomática del ejemplo anterior e introdujo muchos símbolos que requieren mucho pensamiento para comprender realmente lo que está sucediendo.
  • Solo use comentarios cuando tenga que justificar algo inesperado . Sé que sus ejemplos estaban allí para explicar construcciones a personas menos familiarizadas con Ruby, y eso está bien para una pregunta. Prefiero usar comentarios para explicar reglas comerciales inesperadas y evitarlas si el código puede hablar por sí mismo.

Dicho esto, hay momentos en que el código expandido ayuda a comprender mejor lo que está sucediendo. Un ejemplo con eso proviene de C # y LINQ. LINQ es una gran herramienta y puede mejorar la legibilidad en algunas situaciones, pero también me he encontrado con varias situaciones en las que era mucho más confuso. He recibido algunos comentarios en la revisión por pares que sugirió convertir la expresión en un bucle con declaraciones if apropiadas para que otros puedan mantenerla mejor. Cuando cumplí, tenían razón. Técnicamente, LINQ es más idiomático para C #, pero hay casos en los que degrada la comprensibilidad y una solución más detallada lo mejora.

Digo todo eso para decir esto:

Mejore cuando pueda mejorar su código (más comprensible)

Recuerde, usted o alguien como usted tendrá que mantener ese código más adelante. La próxima vez que te encuentres, podrían pasar meses. Hazte un favor y no persigas la reducción de los recuentos de líneas a costa de poder entender tu código.


0

La legibilidad es una propiedad que desea tener, y tener muchas líneas no lo es. Entonces, en lugar de "one-liners vs legibilidad" la pregunta debería ser:

¿Cuándo aumentan la legibilidad los one-liners y cuándo la dañan?

Creo que las frases sencillas son buenas para la legibilidad cuando cumplen estas dos condiciones:

  1. Son demasiado específicos para ser extraídos a una función.
  2. No desea interrumpir el "flujo" de leer el código circundante.

Por ejemplo, digamos que nameno fue un buen ... nombre para su método. Que combinar el nombre y el apellido, o usar el correo electrónico en lugar del nombre, no era algo natural. Entonces, en lugar de namelo mejor que se te ocurrió, resultó largo y engorroso:

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

Un nombre tan largo indica que es muy específico: si fuera más general, podría haber encontrado un nombre más general. Por lo tanto, envolverlo en un método no ayuda con la legibilidad (es demasiado larga) ni con DRYness (demasiado específico para usar en otro lugar), por lo que es mejor dejar el código allí.

Aún así, ¿por qué hacerlo de una sola línea? Por lo general, son menos legibles que el código multilínea. Aquí es donde deberíamos verificar mi segunda condición: el flujo del código circundante. ¿Qué pasa si tienes algo como esto?

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}"

El código multilínea en sí mismo es legible, pero cuando intenta leer el código circundante (imprimiendo los diversos campos) esa construcción multilínea está interrumpiendo el flujo. Esto es más fácil de leer:

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}"

Su flujo no se interrumpe, y puede enfocarse en la expresión específica si lo necesita.

¿Es este tu caso? ¡Definitivamente no!

La primera condición es menos relevante: ya consideró que es lo suficientemente general como para merecer un método, y se le ocurrió un nombre para ese método que es mucho más legible que su implementación. Obviamente no lo extraerías a una función nuevamente.

En cuanto a la segunda condición, ¿interrumpe el flujo del código circundante? ¡No! El código que lo rodea es una declaración de método, que elegir namees el único propósito. La lógica de elegir el nombre no está interrumpiendo el flujo del código circundante, ¡es el propósito mismo del código circundante!

Conclusión: no haga que todo el cuerpo de la función sea de una sola línea

Las frases sencillas son buenas cuando quieres hacer algo un poco complejo sin interrumpir el flujo. Una declaración de función ya está interrumpiendo el flujo (para que no se interrumpa cuando se llama a esa función), por lo que hacer que todo el cuerpo de la función sea de una sola línea no ayuda a la legibilidad.

Nota

Me refiero a las funciones y métodos "completos", no a las funciones en línea o las expresiones lambda que generalmente son parte del código circundante y deben ajustarse a su flujo.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.