¿Señal mala si nadie puede comprender su código? [duplicar]

53

Si un codificador escribe un código que nadie más que él puede entender, y las revisiones de código siempre terminan con el revisor rascándose la cabeza o sosteniendo su cabeza en sus manos, ¿es esto una señal clara de que el programador simplemente no está recortado? programación profesional? ¿Sería esto suficiente para justificar un cambio de carrera? ¿Qué tan importante es el código comprensible en esta industria?

Considere estos ejemplos de C, compare:

if (b)
  return 42;
else
  return 7;

versus:

return (b * 42) | (~(b - 1) * 7);

¿Hay alguna excusa para emplear el último ejemplo? Si es así, ¿cuándo y por qué?

EDITAR: Dejando el último fragmento de código original para su consideración, agregando una corrección:

return (b * 42) | ((b - 1) & 7);

Creo que la otra observación interesante es que requiere que b sea 1 para verdadero y 0 para falso, cualquier otro valor daría resultados extraños.

    
pregunta Jonathan Neufeld 28.02.2015 - 01:15

9 respuestas

107

La primera regla de cualquier ingeniero de software profesional es escribir código que sea comprensible. El segundo ejemplo parece un ejemplo optimizado para un compilador más viejo y sin optimización o simplemente para alguien que quiere expresarse con operadores de bitwise. Está bastante claro qué sucede si estamos familiarizados con las operaciones bitwise, pero a menos que esté en un dominio donde sea ese el caso, evite el segundo ejemplo. También debo señalar que faltan las llaves en el primer ejemplo.

El codificador puede insistir en que su código sea eficiente, ese puede ser el caso, pero si no se puede mantener, es posible que no se escriba en absoluto, ya que generará una deuda técnica terrible en el futuro. p>     

respondido por el World Engineer 28.02.2015 - 01:24
83

Hay varias preguntas que plantea.

1) ¿Es esto un signo claro de que el programador no está cortado para la programación profesional?

  • no Los desarrolladores a menudo pasan por etapas en las que aprenden sobre una idea y desean aplicarla. ¿Siempre aplican estas ideas de manera eficiente y / o efectiva? No. Se cometen errores, y es parte del proceso de aprendizaje. Si están constantemente escribiendo código incomprensible, se requiere una mejor comunicación. Los revisores deben comunicar cuáles son las expectativas para el código aceptable, y el codificador debe comunicar qué hace (se supone que debe) hacer el código y, si es necesario, por qué se hizo de una manera particular.

2) ¿Sería esto suficiente para justificar un cambio de carrera?

  • No tocar este.

3) ¿Qué tan importante es el código comprensible en esta industria?

  • Extremadamente. Si el código no es comprensible, no puede estar seguro de lo que está haciendo, ni de lo que NO está haciendo. En otras palabras, no tiene forma de saber si está funcionando correctamente.

4a) Los ejemplos de C:

  • Los ejemplos de C citados no son equivalentes. El segundo caso dará los mismos resultados que el primero si y solo si b está restringido a los valores de 0 y 1. Sin embargo, si agrega un valor de por ejemplo 173,406,926 para b , los resultados NO coincidirán.

4b) ¿Hay alguna excusa para emplear el último ejemplo?

  • Sí. Muchos procesadores emplean predicción de ramificaciones y tuberías. El costo de una sucursal predicha incorrectamente puede introducir retrasos inaceptables. Para lograr una respuesta más determinista, el twiddling de bits podría usarse para suavizar las cosas.

  • Mi propia opinión sobre el segundo ejemplo es que es innecesariamente complicado y debería ser revisado para mayor claridad. Además, no me gusta la multiplicación ya que (dependiendo de la arquitectura) puede ser lenta en comparación con otras operaciones. Lo más probable es que prefiero ver algo de la forma:

    return b ? 42 : 7;
    
  • Sin embargo, dependiendo de la circunstancia (Y si se pudiera demostrar que proporcionó mejores resultados sustancialmente mejores en categorías críticas), podría aceptar una macro con un nombre descriptivo apropiado al revisar el código. Por ejemplo:

    /*
     * APPROPRIATE_MACRO_NAME - use (x) if (condition) is 1, (y) if (condition) is 0
     *
     * Parameter Restrictions:
     * (condition) - must be either 1 or 0
     * (x) and (y) are of the same integer type
     *
     * This macro generates code that avoids branching (and thus incorrectly
     * predicted branches).  Its use should be restricted to <such and such>
     * because <why and why>.
     */
    #define APPROPRIATE_MACRO_NAME(condition,x,y)  \
        (((condition - 1) & ((y) - (x))) + (x))
    

Espero que esto ayude.

    
respondido por el Sparky 28.02.2015 - 02:46
38

El segundo código no devuelve 42 o 7.

for b = 1:
  (1 * 42) | (~(1 - 1) * 7)
  42 | (~(0) * 7) 
  42 | (-1 * 7) 
  42 | -7
  -5

for b = 0:
  (0 * 42) | (~(0 - 1) * 7)
  0 | (~(-1) * 7) 
  0 | (0 * 7) 
  0 | 0
  0

Y, sin embargo, cuando lo publicaste, creíste que lo hizo, por eso es exactamente por lo que debes evitar el código complicado.

Sin embargo, tome algún código "correcto", por ejemplo, como:

return ((b - 1) & (7 ^ 42)) ^ 42;

Hay dos razones por las que puedo pensar que puede ser útil. - La arquitectura para la que está escribiendo no admite instrucciones de bifurcación o predicadas. - La arquitectura para la que está escribiendo tiene una canalización que no funciona más allá de una operación de sucursal o tiene un costo prohibitivo asociado con una falla de predicción de sucursal.

En este caso, escribirías algo en este sentido:

/* 
   This code is equivalent to:

   if (b)
      return 42;
   else
      return 7;

   when b=0 or b=1

   But does not include a branch instruction since a branch prediction
   miss here would cause an unacceptable impact to performance. 
*/

return ((b - 1) & (7 ^ 42)) ^ 42;

Sin embargo, si solo ves un código como ese sin explicación o razón, es probable que sea un signo de ofuscación del código fuente. La ofuscación de código fuente (a diferencia de la ofuscación de código binario, que muchos considerarían tener propósitos legítimos) tiende a ser nefasta. Algunos programadores pueden ser territoriales por diversas razones, a veces por seguridad laboral y otras por un mayor control sobre lo que se hace y cómo se hacen las cosas debido al ego, la inseguridad o la desconfianza. Sin embargo, casi siempre está en contra de los intereses de los compañeros y del empleador. Es responsabilidad de quienquiera que esté a cargo del equipo eliminar este comportamiento más temprano que tarde, ya sea creando confianza mutua o inculcando miedo, según el estilo de gestión del líder y los métodos a los que responde el programador individual.

    
respondido por el Donscarletti 28.02.2015 - 06:00
6

Lo más probable es que alguien que escribe (b * 42) | (~(b - 1) * 7) sea alguien que sepa muy poco acerca de la programación que trata de pretender tener experiencia / conocimiento / etc, o alguien que intente sabotear un proyecto (es decir, que tenga demasiada experiencia / conocimiento / inteligencia) y quiero seguridad en el empleo).

El primer tipo de persona quiere mostrar que sabe cómo usar NO, O, orden de operaciones, etc. Están demostrando su conocimiento, pero, por desgracia, están escribiendo un código que es mucho menos eficiente, porque esto requiere dos multiplicaciones, una resta, una no y una o, que es claramente menos eficiente que una comparación, un par de saltos y una devolución.

Si ese es el caso, no merecen estar en la industria (todavía), pero su demostración demuestra que conocen la lógica básica de la computadora y que podrían ser un recurso valioso algún día, una vez que superen el showboating y comiencen a escribir código eficiente . Además, existe la clara posibilidad de que b no necesariamente sea 0 o 1, lo que daría como resultado que se devuelva un valor completamente diferente. Esto podría introducir errores sutiles que pueden ser difíciles de encontrar.

El segundo tipo de persona espera introducir un código como este (o muchos otros tipos de código tortuosos), para que las personas sigan haciéndoles preguntas sobre el código, por lo que se consideran demasiado valiosas para perderlas. Este tipo de sabotaje terminará dañando un proyecto, y se les debe dejar ir de inmediato hasta que aprendan la lección y escriban un código optimizado y fácil de leer. También existe la posibilidad de que b no sea 1 o 0, como antes, lo que significa que devolverá un valor diferente al esperado (42 o 7), que puede funcionar correctamente hasta que algún programador desafortunado lo vuelva a cambiar a if(b) ... else ... y el código de repente deja de funcionar. Por ejemplo, tal vez este es un generador de pseudo-números disfrazado como una declaración muy costosa si.

El código legible es importante, así como el código libre (tan práctico) de errores lógicos como este. Cualquiera que haya escrito un código serio durante un tiempo sabe lo importante que es esto. Escribe un juego completamente funcional de Tic Tac Toe. Ahora, ponga este código a un lado por un año, luego vuelva a él e intente leer el código. Si lo escribiera por casualidad, sin tener en cuenta los estándares de codificación, los comentarios, la documentación, etc., es probable que ni siquiera reconozca que escribió el código que escribió, y mucho menos cómo solucionarlo o agregar una nueva función si se rompe algo. necesitaba ser actualizado.

Cuando varios desarrolladores trabajan juntos, es aún más importante que el código sea legible, porque las probabilidades son que no mantendrás ese código. Se habrá mudado a otras cosas, y alguien más tendrá que mantener su código. A la inversa, puede heredar otro proyecto y esperará que los desarrolladores antes de dejar comentarios y código limpio puedan trabajar con usted. Los programadores que trabajan en un código confiable escriben el código que se puede mantener, incluidos la legibilidad y los comentarios.

El rendimiento, aunque también es importante, no debe superar la legibilidad. Como mínimo, si alguien usara el segundo bloque de código aquí, esperaría un bloque de comentarios largo que explique claramente por qué lo hicieron de esta manera en lugar de una manera más convencional. En ese momento, probablemente decidiría reemplazarlo con un código más convencional si no hubiera una buena razón para ello. Si, de hecho, fuera una bomba lógica, la reescribiría de una manera más larga para que se entienda que la función debe ser de esa forma para evitar errores, junto con una documentación clara de lo que realmente hace.

Resulta que estoy bastante seguro de que existe un uso legítimo para un problema especializado que, por casualidad, funciona para necesitar este algoritmo preciso. Sin embargo, si es así, esperaría comentarios que expliquen el algoritmo que usa esta lógica, y es mejor que sea para algo mejor que un bloque if-else. Los únicos dos bloques if-else apropiados para el ejemplo específico son: if(b) return 42; return 7; (de lo contrario es opcional) y return b? 42: 7; (los operadores ternarios están bien para la lógica de ramificación pequeña, a menos que los estándares de codificación lo prohíban por completo). Cualquier otra cosa es demasiado complicada y debería reducirse a una declaración más simple.

Ocasionalmente, me encuentro escribiendo un código "complicado" que los desarrolladores más jóvenes pueden no entender de inmediato, y generalmente comento ese código para que puedan entender por qué se escribió de la manera en que lo fue. A veces, el código optimizado puede ser difícil de leer y, sin embargo, es necesario, pero esta debería ser la excepción y no la regla.

Hay, casualmente, un lugar perfectamente aceptable para un código como este. Concursos de ofuscación. En ese caso, me reservaría un criterio para la función hasta que determinara que el código era simplemente una rama muy inteligente que desperdiciaba la CPU, o si era un dispositivo más desviado para generar números pseudoaleatorios, el valor para PI (o algún otro número mágico), o tal vez incluso un algoritmo de cifrado débil.

    
respondido por el phyrfox 28.02.2015 - 06:44
4

Si las ramas en if / then / else son un problema, probablemente sea más fácil simplemente cambiar a algo como:

static const int values[] = {6, 42};

return values[b!=0];

Esto realmente funciona y, aunque a algunos les resulte menos legible que if / then / else , ciertamente no debería ser una obstrucción notable para nadie que sepa C o C ++ en absoluto.

Para cualquiera que piense que se trata de un truco sucio, o depende de una verificación de tipos particularmente vaga en un idioma en particular: sí y no. Como está escrito, el código depende de que "falso" se convierta a 0 y "verdadero" a 1 en C y C ++.

Sin embargo, la idea básica puede aplicarse igualmente bien en otros idiomas, incluidos aquellos que tienen sistemas de tipo sustancialmente más estrictos. Por ejemplo, en Pascal se vería la misma idea básica:

var
    values : array [boolean] of Integer;

(* ... *)
values[false] = 6;
values[true] = 42;

(* ... *)
f = values[b<>0];

El Pascal User Manual and Report (2ª edición nd ) de Kathleen Jensen y Niklaus Wirth muestra el uso de un Booleano como un índice en una matriz en una serie de lugares, como §6.2.1 y §7. Aunque Pascal (como se definió originalmente) no incluye la noción de variables inicializadas, si lo hiciera, los inicializadores terminarían en el mismo orden que lo hacen en C y C ++ (ya que define el hecho de que false < true ).

Ada usa una sintaxis ligeramente diferente, pero proporciona la misma capacidad básica:

Temp    : array(Boolean) of Integer := (false => 7, true=>42);

-- ...

Return_Value = Temp( 0 /= i);

Así que no, no estamos tratando con algo que es solo un accidente de un idioma que utiliza el control de tipos particularmente suelto. Pascal y (especialmente) Ada son bien conocidos por ser particularmente fuertemente tipados, pero ambos soportan el mismo constructo básico que C y C ++, y lo hacen esencialmente de la misma manera.

    
respondido por el Jerry Coffin 28.02.2015 - 04:20
3

Si me presentaran un código como este en una revisión de código, tendría dos preguntas para plantear:

  • ¿Por qué elegimos escribirlo de esta manera? Dado que la manipulación de bits como esta se utiliza para evitar algún tipo de cuello de botella en el rendimiento, se podría suponer que tenemos un cuello de botella que se rectifica si empleamos este enfoque. Una pregunta de seguimiento inmediata sería: "¿Cómo probamos que esto era un cuello de botella crítico?"

  • ¿Tenemos algún tipo de marco de aceptación (pruebas unitarias, etc.) que demuestre que el enfoque optimizado es equivalente al enfoque no optimizado? Si no no , es cuando se activan las alarmas, pero no son tan fuertes como uno podría considerar.

En última instancia, el código que escribe debe ser mantenible . El código que es rápido pero difícil de abordar cuando ocurre un error no es un buen código. Si pudieran satisfacer mis dos preocupaciones anteriores, entonces probablemente lo dejaría ir con una solicitud para agregar tanto la justificación como su forma equivalente, sin bits.

En general, una revisión del código debe identificar y aclarar estas deficiencias; Si no hubo una razón justificable para escribir el código de esa manera, o el código simplemente no es correcto (como algunos otros han señalado aquí), no hay razón para haberlo escrito de esa manera en primer lugar, y debe corregirse. . Si esta es una tendencia continua; es decir, un desarrollador continúa escribiendo código que es eficiente pero que está horriblemente ofuscado, en ese momento, su líder o alta gerencia debería intervenir y reiterar la importancia del código claro.

    
respondido por el Makoto 01.03.2015 - 21:41
3

Algo como lo siguiente sería una manera de hacer más evidente el INTENTO del código:

manifoldPressureFloor = (b * 42) | (~(b - 1) * 7);

return manifoldPressureFloor;

manifoldPressureFloor está totalmente creado, por supuesto, no tengo ni idea de qué trata realmente el código original.

Pero sin algún tipo de explicación o justificación para el código ofuscado, el revisor del código y / o el programador de mantenimiento están en la posición de tener ninguna idea clara de lo que el programador original realmente quería lograr , lo cual a su vez, hace que sea casi imposible probar que el código realmente funciona (que en realidad hace lo que se supone que debe hacer). Y hace que la programación de mantenimiento sea tanto dolorosa como mucho más propensa a introducir errores.

No creo (sin calificaciones) que el código pueda ser completamente auto comentado. Sin embargo; Realmente creo que es posible apoyarse mucho en esa dirección.

Si hay alguna infrecuente (o ventaja, rendimiento u otra no especificada hasta ahora) por la que se justifica la sintaxis (b * 42) | (~(b - 1) * 7) , el revisor del código no pudo captar fácilmente ese hecho porque la INTENCIÓN del código no se descifra fácilmente , y aparentemente no hubo comentarios que explicaran por qué se hizo eso.

Debe evitarse el código que es inteligente solo por ser inteligente. Uno de los propósitos principales de escribir un código claro es garantizar la comprensión humana y evitar de manera proactiva la introducción de errores en el futuro. Un código inteligente que no es claramente comprensible es una bomba de tiempo. Incluso si funciona hoy, dejará de funcionar después del mantenimiento del código mañana o dentro de un año. Y los errores serán esotéricos y difíciles de encontrar y solucionar.

¿Cómo puede alguien que no sea el programador original probar que el código funciona si la intención y la sintaxis no están claras? ¿Cómo puede el programador original probar que el código funciona en ese caso?

Los comentarios deben ser requeridos en el código de caso de borde inteligente y enroscable. Pero es mejor si el código es lo suficientemente simple como para no requerir los comentarios. Esos comentarios también terminan desactualizados o son irrelevantes si el código se actualiza y el programador de mantenimiento no actualiza ni elimina los comentarios. Los programadores actualizan el código de forma rutinaria sin actualizar los comentarios. Ellos no deberían , simplemente hacen .

Todos estos factores llevan a la conclusión de que este programador debe abordar los problemas de poder probar que el código funciona, hacer que otros seres humanos lo puedan comprender y hacer que el código sea sólido. . El código que es altamente susceptible a la introducción de nuevos errores durante el mantenimiento es frágil. Ningún empleador quiere pagar mucho dinero por el desarrollo de un código frágil y propenso a errores.

El código frágil, cuyo comportamiento no es demostrable, es mucho más costoso que el código limpio, claro, demostrable (comprobable), fácil de comprender, fácil de mantener. Alguien más le paga al programador para que sea un profesional y produzca un producto razonablemente limpio, robusto y mantenible a un costo razonable. Derecho?

En conclusión, el programador que creó ese código es obviamente inteligente y, por lo tanto, lleno de potencial. Pero los problemas de poder probar la corrección del código, hacen que el código sea robusto y comprensible, y que se debe tomar en serio. Si ese cambio puede tener lugar, es probable que el programador valga la pena mantenerlo. Si no, y el programador insiste en seguir haciendo las cosas de esta manera, podría ser difícil justificar que se queden en el equipo, ya que es casi seguro que le costarán más de lo que lo harán a la larga. Y ese es el verdadero problema, ¿no?

IMHO.

    
respondido por el Craig 28.02.2015 - 06:46
2

El reemplazo de if-s con expresiones aritméticas / lógicas a veces es necesario cuando se requiere una tubería de procesador larga. Eso hace que el código siempre ejecute las mismas instrucciones independientemente de la condición, lo que lo hace más adecuado para la paralelización.

Dicho esto, la muestra proporcionada es incorrecta, ya que las dos muestras no son equivalentes:

if (b)
  return 42;
else
  return 7;

puede ser return ((b&1)*42)|((1-(b&1)))*7

El programador de la muestra de OP puede haber hecho una eliminación if if incorrecta, o la OP en sí puede haber malinterpretado las intenciones del programador al adivinar el conmutador if traditional de manera incorrecta. Decir cuál de los dos es correcto es difícil no saber cuál era el requisito.

Tenga en cuenta que la expresión no es tan "oscura": es solo una combinación lineal en la forma P*(t)+Q*(1-t) (con t = 0..1) que todo programador debe conocer, ya que es la base de la mayoría de algebra lineal.

Lo único que puedo entender es que entre el programador y el revisor hay una base cultural diferente sobre el procesamiento paralelo y el álgebra lineal. En mi humilde opinión, no se hace una superior a la otra, si se demuestra la corrección.

    
respondido por el Emilio Garavaglia 28.02.2015 - 22:51
1

Además de los excelentes puntos hechos por Sparky (ítems 1 y 3) y Donscarletti, este post me lleva a otro que contesté hace mucho tiempo: ¿Cómo documento mi código? .

Muchas personas son o se llaman programadores, algunas son buenas, no muchas son excelentes. Al igual que en muchos otros ámbitos de la vida. Puedes decidir juzgar a aquellos que parecen menos buenos de lo que eres o menos buenos de lo que esperas que sean (no son muy útiles, es una pérdida de tiempo), puedes intentar ayudarlos (grandes) o obligarlos a hacerlo mejor ( es posible que no tenga otra opción), o ignórelos y simplemente haga su mejor esfuerzo (es posible que no tenga otra opción, esta es a veces la mejor ruta). Cualquiera que sea su elección de acción, generalmente depende de muchos factores que van más allá de la simple capacidad técnica.

    
respondido por el asoundmove 02.03.2015 - 03:58

Lea otras preguntas en las etiquetas