Capacidad de lectura versus capacidad de mantenimiento, caso especial de escritura de llamadas de función anidadas

57

Mi estilo de codificación para llamadas de función 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 he cambiado a un departamento donde el siguiente estilo de codificación está muy en uso:

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

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

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

Por otra parte, cuanto más procesamiento pones en una línea, más lógica obtienes en una página, lo que mejora la legibilidad.

¿Mi miedo es correcto o me falta algo y, en general, cuál es el preferido en un entorno comercial? ¿Capacidad de lectura o mantenimiento?

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

    
pregunta Dominique 22.02.2018 - 12:27

9 respuestas

111

Si te sentiste obligado a expandir una sola línea como

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

No te culparía. 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 al mismo tiempo
  3. Está libre de nombres descriptivos

Si lo amplías 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 todavía 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 al mismo tiempo
  3. Está libre de nombres descriptivos
  4. Está lleno de nombres no descriptivos

Si lo amplías con nombres que agregan un significado nuevo, bueno y semántico, ¡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 ofrece aquí, pero requiere que hagas los nombres.

Si lo haces con nombres sin sentido como result_this y result_that porque simplemente no puedes pensar en nombres buenos, entonces realmente preferiría que nos ahorres el desorden de nombres sin sentido y expandirlos usando algunos espacios en blanco viejos y buenos:

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

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

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

Cuando no puedes pensar en buenos nombres, eso es lo mejor que hay.

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

Siesonoessuficiente,imaginequesellamóaG2()enmásdeunlugaryluegosucedió:

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 llamada G2() estaría en su propia línea, este estilo lo lleva directamente a la llamada ofensiva en la parte principal.

Entonces, no use los problemas 1 y 2 como excusa para seguir con el problema 4. Use buenos nombres cuando pueda pensar en ellos. Evita los nombres sin sentido cuando no puedes.

Razas de claridad en la órbita comentar 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 esa corriente de ruido, incluso cuando no se necesita el ajuste de 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, he encontrado que este estilo funciona bien con el código funcional que se está moviendo hacia el espacio orientado a objetos. Si puede encontrar buenos nombres para hacer eso en un estilo intermedio, entonces tendrá más poder para usted. Hasta entonces estoy usando esto. Pero en cualquier caso, por favor, encuentre alguna manera de evitar nombres de resultados sin sentido. Me hacen doler los ojos.

    
respondido por el candied_orange 22.02.2018 - 14:54
50
  

Por otra parte, cuanto más procesamiento pones en una línea, más lógica obtienes en una página, lo que mejora la legibilidad.

Estoy totalmente en desacuerdo con esto. El solo hecho de ver los dos ejemplos de código dice que esto es incorrecto:

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

se escucha para leer. "Legibilidad" no significa densidad de información; significa "fácil de leer, entender 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 diciendo que "fácil de diagnosticar fallos" significa 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 que ha sido fácil de leer y entender, respaldado por un buen conjunto de pruebas automatizadas.

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

    
respondido por el David Arno 22.02.2018 - 12:45
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 en ese sentido, no podemos decirlo. De todos modos, es largo aliento debido a una densidad de información extremadamente baja, que generalmente no se presta a una fácil comprensión.

Tu segundo ejemplo está condensado en un grado absurdo. Si las funciones tuvieran nombres útiles, eso podría estar bien y ser legible porque no hay demasiado , sino que es confuso en la otra dirección.

Después de introducir nombres significativos, puede ver 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 ocultarse de usted.

    
respondido por el Deduplicator 22.02.2018 - 13:08
17

Como siempre, cuando se trata de la legibilidad, el error está en los extremos . Puede seguir los buenos consejos de programación de cualquier , convertirlo en una regla religiosa y usarlo para producir un código totalmente ilegible. (Si no me crees en esto, echa un vistazo a estos dos ganadores de IOCCC borsanyi y goren y eche un vistazo a cuán diferentes usan las funciones para hacer que el código sea totalmente ilegible. Sugerencia: Borsanyi usa exactamente una función, mucho más, mucho más ...)

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

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

El punto es que no hay una sola complejidad de enunciado medible que pueda decidir qué es bueno incluir en una sola afirmación. Tomemos, por ejemplo, la línea:

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

Esta es una afirmación bastante compleja, pero cualquier programador que valga su 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 patrón bien conocido 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, a pesar de que es una operación menos que el cálculo de la distancia . Por supuesto, eso es una consecuencia directa de que yo no sepa nada sobre F() , G1() , G2() , H1() o H2() . Podría decidir de otra manera 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.

    
respondido por el cmaster 23.02.2018 - 15:07
14

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

¿Es posible tener un código que sea mantenible pero ilegible? A la inversa, si el código es extremadamente legible, ¿por qué no se puede mantener debido a que es legible? Nunca he escuchado de ningún programador que haya enfrentado estos factores, ¡teniendo que elegir uno u otro!

En cuanto a decidir si usar variables intermedias para llamadas de función 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.

Yo 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 ser anidadas que las funciones que representan una lógica de dominio oscura cuyos resultados son impredecibles y el lector no puede evaluarlas mentalmente.

  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. Las funciones de mezcla de diferentes aridades en diferentes niveles de anidación tienden a dejar el código como una oreja de cerdo.

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

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

respondido por el Steve 22.02.2018 - 15:37
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 funciones generales:

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

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

Por ejemplo, no hay ningún valor agregado al mover conversiones de unidades / tipos simples a sus propias líneas, porque son fáciles de leer y es muy poco probable que fallen:

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

En lo que respecta a su preocupación de analizar los volcados de fallos, la validación de entrada suele ser mucho más importante: es probable que el bloqueo real ocurra dentro de estas funciones en lugar de que la línea las llame, e incluso si no es así, generalmente no necesita estar dijo exactamente donde las cosas explotaron. Es mucho más importante saber dónde empezaron a desmoronarse las cosas, que saber dónde explotaron finalmente, que es lo que atrapa la validación de entrada.

    
respondido por el Peter 23.02.2018 - 15:43
1

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

Yo pondría la legibilidad en algún lugar entre 80 y 90 de mantenibilidad. El otro 10-20 por ciento es lo conveniente que es la refactorización.

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

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 lógica MENOS que contiene un método es más legible.

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

    
respondido por el jmoreno 23.02.2018 - 03:00
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));

Puedes escribir expresiones en línea, y aún así ser señalado donde el problema es simplemente mirando el seguimiento 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, pero 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 está tan mal a mitad de camino.

Sin mencionar que el grupo de funciones de ajuste puede 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));
    
respondido por el GameDeveloper 25.02.2018 - 09:45
1

En mi opinión, el código de auto-documentación es mejor tanto para la facilidad de mantenimiento como para la legibilidad, sin importar el 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 variables y nombres de funciones que establezcan claramente su propósito es invaluable.

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

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

Este 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 aleatoria de caracteres, vista desde una perspectiva de autodocumentación:

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

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

Creo que un buen diseño de código es un gran desafío porque no hay reglas estrictas que puedan ser detectadas y aplicadas sistemáticamente.

    
respondido por el Ozymandias 27.02.2018 - 10:41

Lea otras preguntas en las etiquetas