Legibilidad versus mantenibilidad, caso especial de escribir llamadas a funciones anidadas

57

Mi estilo de codificación para llamadas a funciones anidadas es el siguiente:

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);

Recientemente me cambié a un departamento donde se usa mucho el siguiente estilo de codificación:

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

El resultado de mi forma de codificar es que, en caso de una función de bloqueo, Visual Studio puede abrir el volcado correspondiente e indicar la línea donde se produce el problema (estoy especialmente preocupado por las violaciones de acceso).

Me temo que, en caso de un bloqueo debido al mismo problema programado en la primera forma, no podré saber qué función ha causado el bloqueo.

Por otro lado, cuanto más procesamiento ponga en una línea, más lógica obtendrá en una página, lo que mejora la legibilidad.

¿Es correcto mi miedo o me falta algo y, en general, cuál es el preferido en un entorno comercial? ¿Legibilidad o mantenibilidad?

No sé si es relevante, pero estamos trabajando en C ++ (STL) / C #.

Dominique
fuente
17
@gnat: se refiere a una pregunta general, aunque estoy especialmente interesado en el caso mencionado de llamadas a funciones anidadas y la consecuencia en caso de análisis de volcado por caída, pero gracias por el enlace, contiene información bastante interesante.
Dominique
99
Tenga en cuenta que si este ejemplo se aplicara a C ++ (como se menciono esto está utilizando en su proyecto), entonces esto no es sólo una cuestión de estilo, ya que el orden de evaluación de los HXy GXlas invocaciones puede cambiar en el de una sola línea, como El orden de evaluación de los argumentos de la función no está especificado. Si por alguna razón depende del orden de los efectos secundarios (a sabiendas o sin saberlo) en las invocaciones, esta "refactorización de estilo" podría terminar afectando más que la legibilidad / mantenimiento.
dfri
44
¿Es el nombre de la variable result_g1lo que realmente usarías o este valor realmente representa algo con un nombre sensible? por ej percentageIncreasePerSecond. Esa sería mi prueba para decidir entre los dos
Richard Tingle
3
Independientemente de sus sentimientos sobre el estilo de codificación, debe seguir la convención que ya está vigente a menos que sea claramente errónea (no parece que sea en este caso).
n00b
44
@ t3chb0t Puedes votar como quieras, pero ten en cuenta el interés de alentar preguntas buenas, útiles y sobre el tema en este sitio (y desalentar las malas), que el propósito de votar hacia arriba o hacia abajo es una pregunta para indicar si una pregunta es útil y clara, por lo que votar por otros motivos, como usar un voto como un medio para criticar algún código de ejemplo publicado para ayudar al contexto de la pregunta, generalmente no es útil para mantener la calidad del sitio : softwareengineering.stackexchange.com/help/privileges/vote-down
Ben Cottrell

Respuestas:

111

Si te sentiste obligado a expandir un trazador de líneas como

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

No te culpo. Eso no solo es difícil de leer, es difícil de depurar.

¿Por qué?

  1. Es denso
  2. Algunos depuradores solo resaltarán todo de una vez
  3. Está libre de nombres descriptivos.

Si lo expandes con resultados intermedios obtienes

 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);

y aún es difícil de leer ¿Por qué? Resuelve dos de los problemas e introduce un cuarto:

  1. Es denso
  2. Algunos depuradores solo resaltarán todo de una vez
  3. Está libre de nombres descriptivos.
  4. Está lleno de nombres no descriptivos

Si lo expande con nombres que agreguen un significado semántico nuevo, bueno, ¡incluso mejor! Un buen nombre me ayuda a entender.

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

Ahora al menos esto cuenta una historia. Soluciona los problemas y es claramente mejor que cualquier otra cosa que se ofrezca aquí, pero requiere que se te ocurran los nombres.

Si lo hace con nombres sin sentido como result_thisy result_thatporque simplemente no puede pensar en buenos nombres, entonces realmente preferiría que nos ahorre el desorden de nombres sin sentido y lo expanda usando un buen espacio en blanco:

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

Es igual de legible, si no más, que el que tiene nombres de resultados sin sentido (no es que estos nombres de funciones sean tan geniales).

  1. Es denso
  2. Algunos depuradores solo resaltarán todo de una vez
  3. Está libre de nombres descriptivos.
  4. Está lleno de nombres no descriptivos

Cuando no se te ocurren buenos nombres, es tan bueno como parece.

Por alguna razón, a los depuradores les encantan las nuevas líneas, por lo que debería encontrar que depurar esto no es difícil:

ingrese la descripción de la imagen aquí

Si eso no es suficiente, imagine que G2()se llamó en más de un lugar y esto sucedió:

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

Creo que es bueno que, dado que cada G2()llamada estaría en su propia línea, este estilo lo lleva directamente a la llamada infractora en general.

Por lo tanto, no use los problemas 1 y 2 como excusa para seguir con el problema 4. Use buenos nombres cuando pueda pensar en ellos. Evite nombres sin sentido cuando no pueda.

Lightness Races en el comentario de Orbit señala correctamente que estas funciones son artificiales y tienen nombres muy pobres. Así que aquí hay un ejemplo de cómo aplicar este estilo a algún código de la naturaleza:

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();

Odio mirar ese flujo de ruido, incluso cuando no es necesario ajustar las palabras. Así es como se ve bajo este 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 puede ver, descubrí que este estilo funciona bien con el código funcional que se mueve en el espacio orientado a objetos. Si puedes encontrar buenos nombres para hacer eso en un estilo intermedio, entonces tienes más poder para ti. Hasta entonces estoy usando esto. Pero en cualquier caso, por favor, encuentre alguna forma de evitar nombres de resultados sin sentido. Hacen que me duelan los ojos.

naranja confitada
fuente
20
@ Steve y no te estoy diciendo que no lo hagas. Estoy rogando por un nombre significativo. A menudo he visto el estilo intermedio hecho sin pensar. Los malos nombres me queman el cerebro mucho más que el escaso código por línea. No dejo que las consideraciones de ancho o largo me motiven a hacer mi código denso o mis nombres cortos. Dejo que me motiven a descomponerme más. Si los buenos nombres no van a suceder, considere esta solución para evitar ruidos sin sentido.
candied_orange
66
Agrego a su publicación: Tengo una pequeña regla general: si no puede nombrarlo, podría ser una señal de que no está bien definido. Lo uso en entidades, propiedades, variables, módulos, menús, clases auxiliares, métodos, etc. En numerosas situaciones, esta pequeña regla ha revelado una falla grave en el diseño. Entonces, en cierto modo, una buena denominación no solo contribuye a la legibilidad y la facilidad de mantenimiento, sino que le ayuda a verificar el diseño. Por supuesto, hay excepciones a cada regla simple.
Alireza
44
La versión ampliada se ve fea. Hay demasiado espacio en blanco allí, lo que reduce su efectividad una vez que todo se pone en fase, lo que significa que no hay nada más.
Mateen Ulhaq
55
@MateenUlhaq El único espacio en blanco adicional que hay es un par de líneas nuevas y algo de sangría, y todo se coloca cuidadosamente en límites significativos . Su comentario, en cambio, coloca espacios en blanco en límites no significativos. Le sugiero que eche un vistazo un poco más cerca y más abierto.
jpmc26
3
A diferencia de @MateenUlhaq, estoy en la cerca del espacio en blanco en este ejemplo particular con tales nombres de funciones, pero con nombres de funciones reales (que tienen más de dos caracteres de longitud, ¿verdad?) Podría ser lo que yo buscaría.
Lightness compite con Monica
50

Por otro lado, cuanto más procesamiento ponga en una línea, más lógica obtendrá en una página, lo que mejora la legibilidad.

Estoy totalmente en desacuerdo con esto. Solo mirar sus dos ejemplos de código dice que esto es incorrecto:

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

Se escucha leer. "Legibilidad" no significa densidad de información; significa "fácil de leer, comprender y mantener".

A veces, el código es simple y tiene sentido usar una sola línea. Otras veces, hacerlo solo hace que sea más difícil de leer, sin ningún beneficio obvio más allá de agrupar más en una línea.

Sin embargo, también lo llamaría al afirmar que "los bloqueos fáciles de diagnosticar" significan que el código es fácil de mantener. El código que no falla es mucho más fácil de mantener. "Fácil de mantener" se logra principalmente a través del código, fácil de leer y comprender, respaldado con un buen conjunto de pruebas automatizadas.

Entonces, si está convirtiendo una sola expresión en una de varias líneas con muchas variables solo porque su código a menudo falla y necesita una mejor información de depuración, entonces deje de hacerlo y haga que el código sea más robusto. Debería preferir escribir código que no necesite depuración sobre código que sea fácil de depurar.

David Arno
fuente
37
Si bien estoy de acuerdo en que F(G1(H1(b1), H2(b2)), G2(c1))es difícil de leer, esto no tiene nada que ver con estar demasiado abarrotado. (No estoy seguro si quería decir eso, pero podría interpretarse de esta manera). Anidar tres o cuatro funciones en una sola línea puede ser perfectamente legible, en particular si algunas de las funciones son simples operadores infijos. El problema aquí son los nombres no descriptivos, pero ese problema es aún peor en la versión de varias líneas, donde se introducen aún más nombres no descriptivos . Agregar solo repetitivo casi nunca ayuda a la legibilidad.
Leftaroundabout
23
@leftaroundabout: Para mí, la dificultad es que no es obvio si G1toma 3 parámetros o solo 2 y G2es otro parámetro para F. Tengo que entrecerrar los ojos y contar los paréntesis.
Matthieu M.
44
@MatthieuM. Esto puede ser un problema, aunque si las funciones son bien conocidas, a menudo es obvio cuál toma cuántos argumentos. Específicamente, como dije, para las funciones de infijo está claro de inmediato que toman dos argumentos. (Además, las tuplas entre paréntesis sintaxis mayoría de lenguajes usan exacerba este problema, en un idioma que prefiere Currying es más claro automáticamente: F (G1 (H1 b1) (H2 b2)) (G2 c1).)
leftaroundabout
55
Personalmente, prefiero la forma más compacta, siempre que haya un estilo alrededor como en mi comentario anterior, porque garantiza menos estado para realizar un seguimiento mental: result_h1no se puede reutilizar si no existe, y la plomería entre las 4 variables es obvio.
Izkata
8
He descubierto que el código que es fácil de depurar generalmente es código que no necesita depuración.
Rob K
25

Su primer ejemplo, la forma de asignación única, es ilegible porque los nombres elegidos no tienen ningún significado. Eso podría ser un artefacto de tratar de no revelar información interna de su parte, el verdadero código podría estar bien a ese respecto, no podemos decirlo. De todos modos, es de largo aliento debido a la densidad de información extremadamente baja, que generalmente no se presta para una fácil comprensión.

Su segundo ejemplo está condensado en un grado absurdo. Si las funciones tenían nombres útiles, eso podría estar bien y ser legible porque no hay demasiado , pero tal como es es confuso en la otra dirección.

Después de introducir nombres significativos, puede observar si una de las formas parece natural, o si hay un medio dorado para disparar.

Ahora que tiene un código legible, la mayoría de los errores serán obvios, y los otros al menos tendrán más dificultades para esconderse de usted.

Deduplicador
fuente
17

Como siempre, cuando se trata de legibilidad, el fracaso es extremo . Puedes tomar cualquier buen consejo de programación, convertirlo en una regla religiosa y usarlo para producir código completamente ilegible. (Si no me cree en esto, vea a estos dos ganadores de IOCCC , borsanyi y goren, y eche un vistazo a cuán diferente usan las funciones para hacer que el código sea completamente ilegible. Sugerencia: Borsanyi usa exactamente una función, mucho, mucho más ...)

En su caso, los dos extremos son 1) usar solo declaraciones de expresión única y 2) unir todo en declaraciones grandes, concisas y complejas. Cualquiera de los enfoques llevados al extremo hace que su código sea ilegible.

Su tarea, como programador, es lograr un equilibrio . Para cada declaración que escriba, es su tarea responder la pregunta: "¿Es esta declaración fácil de entender y sirve para que mi función sea legible?"


El punto es que no existe una única complejidad de declaración medible que pueda decidir qué es bueno incluir en una sola declaración. Tome por ejemplo la línea:

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

Esta es una declaración bastante compleja, pero cualquier programador que valga la pena debería poder comprender de inmediato lo que hace. Es un patrón bastante conocido. Como tal, es mucho más legible que el 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 rompe el conocido patrón en un número aparentemente sin sentido de pasos simples. Sin embargo, la declaración de su pregunta

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

me parece demasiado complicado, aunque es una operación menos que el cálculo de distancia . Por supuesto, eso es una consecuencia directa de mí sin saber nada acerca de F(), G1(), G2(), H1(), o H2(). Podría decidir de manera diferente si supiera más sobre ellos. Pero ese es precisamente el problema: la complejidad aconsejable de una declaración depende en gran medida del contexto y de las operaciones involucradas. Y usted, como programador, es el que debe analizar este contexto y decidir qué incluir en una sola declaración. Si le importa la legibilidad, no puede descargar esta responsabilidad a alguna regla estática.

cmaster
fuente
14

@Dominique, creo que en el análisis de su pregunta, está cometiendo el error de que "legibilidad" y "mantenibilidad" son dos cosas separadas.

¿Es posible tener un código que sea mantenible pero ilegible? Por el contrario, si el código es extremadamente legible, ¿por qué sería imposible de mantener debido a que es legible? ¡Nunca he oído hablar de ningún programador que se haya enfrentado a estos factores, teniendo que elegir uno u otro!

En términos de decidir si usar variables intermedias para llamadas a funciones anidadas, en el caso de 3 variables dadas, llamadas a 5 funciones separadas y algunas llamadas anidadas a 3 profundas, tendería a usar al menos algunas variables intermedias para desglosar eso, como has hecho

Pero ciertamente no voy tan lejos como para decir que las llamadas a funciones nunca deben anidarse en absoluto. Es una cuestión de juicio en las circunstancias.

Diría que los siguientes puntos se refieren a la sentencia:

  1. Si las funciones llamadas representan operaciones matemáticas estándar, son más capaces de anidarse que las funciones que representan alguna lógica de dominio oscura cuyos resultados son impredecibles y no necesariamente pueden ser evaluados mentalmente por el lector.

  2. Una función con un solo parámetro es más capaz de participar en un nido (ya sea como una función interna o externa) que una función con múltiples parámetros. Mezclar funciones de diferentes aries en diferentes niveles de anidación es propenso a dejar el código como la oreja de un cerdo.

  3. Un conjunto de funciones que los programadores están acostumbrados a ver expresado de una manera particular, tal vez porque representa una técnica o ecuación matemática estándar, que tiene una implementación estándar, puede ser más difícil de leer y verificar si se divide en variables intermedias.

  4. Un pequeño nido de llamadas de función que realiza una funcionalidad simple y que ya es fácil de leer, y luego se descompone en exceso y se atomiza, es capaz de ser más difícil de leer que uno que no se descompuso en absoluto.

Steve
fuente
3
+1 a "¿Es posible tener un código que sea mantenible pero ilegible?". Ese fue mi primer pensamiento también.
RonJohn
4

Ambos son subóptimos. Considere los comentarios.

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

O funciones específicas en lugar de generales:

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

Al decidir qué resultados detallar, tenga en cuenta el costo (copia vs referencia, valor l versus valor r), legibilidad y riesgo, individualmente para cada declaración.

Por ejemplo, no hay valor agregado al mover conversiones simples de unidad / tipo a sus propias líneas, porque son fáciles de leer y es muy improbable que fallen:

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

Con respecto a su preocupación por analizar volcados de memoria, la validación de entrada suele ser mucho más importante: es muy probable que la falla real ocurra dentro de estas funciones en lugar de la línea que las llama, e incluso si no, generalmente no necesita que se le diga exactamente dónde Las cosas explotaron. Es mucho más importante saber dónde las cosas comenzaron a desmoronarse, que saber dónde explotaron finalmente, que es lo que atrapa la validación de entrada.

Peter
fuente
Re el costo de pasar un argumento: hay dos reglas de optimización. 1) No lo hagas. 2) (solo para expertos) Todavía no .
RubberDuck
1

La legibilidad es la mayor parte de la mantenibilidad. ¿Duda de mí? Elija un proyecto grande en un idioma que no conoce (preferiblemente tanto el lenguaje de programación como el lenguaje de los programadores), y vea cómo haría para refactorizarlo ...

Pondría legibilidad como entre 80 y 90 de mantenibilidad. El otro 10-20 por ciento es cuán susceptible es refactorizar.

Dicho esto, efectivamente pasas 2 variables a tu función final (F). Esas 2 variables se crean usando otras 3 variables. Habría sido mejor pasar b1, b2 y c1 a F, si F ya existe, cree D que haga la composición para F y devuelva el resultado. En ese momento, solo es cuestión de darle un buen nombre a D, y no importará qué estilo uses.

En un no relacionado, usted dice que más lógica en la página ayuda a la legibilidad. Eso es incorrecto, la métrica no es la página, es el método, y la MENOS lógica que contiene un método, más legible es.

Legible significa que el programador puede mantener la lógica (entrada, salida y algoritmo) en su cabeza. Cuanto más lo hace, MENOS puede entenderlo un programador. Lea sobre la complejidad ciclomática.

jmoreno
fuente
1
Estoy de acuerdo con todo lo que dices sobre la legibilidad. Pero no estoy de acuerdo en que descifrar una operación lógica en métodos separados, necesariamente la hace más legible que dividirla en líneas separadas (ambas técnicas que pueden , cuando se usan en exceso, hacer que la lógica simple sea menos legible y hacer que todo el programa esté más desordenado) Si divide demasiado las cosas en los métodos, termina emulando macros de lenguaje ensamblador y pierde de vista cómo se integran en su conjunto. Además, en este método separado, aún se enfrentaría al mismo problema: anidar las llamadas o dividirlas en variables intermedias.
Steve
@ Steve: No dije que siempre lo hiciera, pero si está pensando en usar 5 líneas para obtener un valor único, hay una buena probabilidad de que una función sea mejor. En cuanto a las líneas múltiples frente a la línea compleja: si es una función con un buen nombre, ambas funcionarán igual de bien.
jmoreno
1

Independientemente de si está en C # o C ++, siempre que esté en una compilación de depuración, una posible solución es envolver las funciones

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

Puede escribir expresiones en línea y aún así señalar dónde está el problema simplemente mirando el rastro de la pila.

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

Por supuesto, si llama a la misma función varias veces en la misma línea, no puede saber qué función, sin embargo, aún puede identificarla:

  • Mirando los parámetros de la función
  • Si los parámetros son idénticos y la función no tiene efectos secundarios, dos llamadas idénticas se convierten en 2 llamadas idénticas, etc.

Esto no es una bala de plata, pero no es tan malo a mitad de camino.

Sin mencionar que el grupo de funciones de ajuste puede incluso ser más beneficioso para la legibilidad del código:

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

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));
Desarrollador de juegos
fuente
1

En mi opinión, el código autodocumentado es mejor tanto para la mantenibilidad como para la legibilidad, independientemente del idioma.

La declaración dada anteriormente es densa, pero "auto documentada":

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

Cuando se divide en etapas (más fácil de probar, seguramente) pierde todo el contexto como se indicó anteriormente:

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

Y obviamente, el uso de nombres de variables y funciones que indiquen claramente su propósito es invaluable.

Incluso los bloques "si" pueden ser buenos o malos en la autodocumentación. Esto es malo porque no puede forzar fácilmente las primeras 2 condiciones para probar la tercera ... no están relacionadas:

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

Éste tiene más sentido "colectivo" y es más fácil crear condiciones de prueba:

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

Y esta declaración es solo una cadena de caracteres aleatoria, vista desde una perspectiva autodocumentada:

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

Mirando la declaración anterior, la mantenibilidad sigue siendo un desafío importante si las funciones H1 y H2 alteran las mismas "variables de estado del sistema" en lugar de unificarse en una sola función "H", porque alguien eventualmente alterará H1 sin siquiera pensar que hay un Función H2 para mirar y podría romper H2.

Creo que un buen diseño de código es muy desafiante porque no hay reglas estrictas que puedan detectarse y aplicarse sistemáticamente.

Ozymandias
fuente