¿En qué casos menos código no es mejor? [cerrado]

55

Últimamente he refactorizado algunos códigos en el trabajo y pensé que hice un buen trabajo. Bajé 980 líneas de código a 450 y reduje a la mitad el número de clases.

Al mostrar esto a mis colegas, algunos no estuvieron de acuerdo en que esto fuera una mejora.

Dijeron: "menos líneas de código no es necesariamente mejor"

Puedo ver que podría haber casos extremos en los que la gente escribe líneas realmente largas y / o pone todo en un solo método para guardar algunas líneas, pero eso no fue lo que hice. En mi opinión, el código está bien estructurado y es más fácil de comprender / mantener debido a que es la mitad del tamaño.

Me cuesta ver por qué alguien querría trabajar con el doble del código que se requiere para hacer un trabajo, y me pregunto si alguien siente lo mismo que mis colegas y puede presentar algunos buenos argumentos para tener más código en menos ?

PiersyP
fuente
145
El tamaño del código se mide en el tiempo que necesita leerlo y comprenderlo, no las líneas o el número de caracteres.
Bergi el
13
Su pregunta como está escrita es categóricamente demasiado amplia. En su lugar, recomiende escribir uno nuevo sobre los cambios específicos que realizó.
jpmc26
8
Considere un algoritmo rápido de raíz cuadrada inversa . Implementar el método Newton completo con el nombre apropiado de las variables sería mucho más claro y fácil de leer, aunque probablemente contenga más líneas de código. (Tenga en cuenta que en este caso particular, el uso de código inteligente era justificable por preocupaciones de rendimiento).
Maciej Piechotka
65
Hay un sitio completo de intercambio de pila dedicado a responder su pregunta: codegolf.stackexchange.com . :)
Federico Poloni
10
Posible duplicado de ¿En qué punto la brevedad ya no es una virtud?
Bob Tway

Respuestas:

123

Una persona delgada no es necesariamente más saludable que una persona con sobrepeso.

Una historia infantil de 980 líneas es más fácil de leer que una tesis de física de 450 líneas.

Hay muchos atributos que determinan la calidad de su código. Algunos simplemente se calculan, como la complejidad ciclomática y la complejidad de Halstead . Otros se definen más libremente, como cohesión , legibilidad, comprensibilidad, extensibilidad, robustez, corrección, autodocumentación, limpieza, comprobabilidad y muchos más.

Podría ser, por ejemplo, que si bien redujo la longitud total del código, introdujo una complejidad injustificada adicional e hizo que el código fuera más críptico.

Dividir un código largo en pequeños métodos podría ser tan dañino como beneficioso .

Pídale a sus colegas que le brinden comentarios específicos sobre por qué creen que sus esfuerzos de refactorización produjeron un resultado indeseable.

MA Hanin
fuente
1
@PiersyP es solo un FYI, una de las pautas que me enseñaron sobre una buena refactorización es que deberíamos ver la complejidad ciclomática reducida a una raíz cuadrada de lo que originalmente era.
MA Hanin
44
@PiersyP también, no estoy diciendo que su código sea peor o mejor de lo que era. Como un extraño no puedo decirlo realmente. También podría ser que sus colegas son demasiado conservadores y temen su cambio simplemente porque no hicieron el esfuerzo necesario para revisarlo y validarlo. Es por eso que le sugerí que les pidiera comentarios adicionales.
MA Hanin
66
Buen trabajo, muchachos: han establecido que hay un peso "correcto" en alguna parte (el número exacto puede variar). Incluso las publicaciones originales de @Neil dicen "SOBREPESO" en lugar de "cuanto más pesada es una persona", y eso se debe a que HAY un punto óptimo, tal como lo hay con la programación. Agregar código más allá de ese "tamaño correcto" es simplemente desorden, y eliminar líneas debajo de ese punto solo sacrifica la comprensión en aras de la brevedad. Saber exactamente dónde está ese punto ... ESO es lo difícil.
AC
1
El hecho de que no sea necesario no significa que no tenga valor.
Chris Wohlert
1
@Neil En general, tienes razón, pero el "equilibrio" siempre elusivo al que te refieres es algo así como un mito, hablando objetivamente . Todos tienen una idea diferente de lo que es un "buen equilibrio". Claramente, OP pensó que había hecho algo bueno, y sus compañeros de trabajo no lo hicieron, pero estoy seguro de que todos pensaron que tenían el "equilibrio correcto" cuando escribieron el código.
code_dredd
35

Curiosamente, un colega y yo estamos actualmente en medio de un refactorizador que aumentará el número de clases y funciones en un poco menos del doble, aunque las líneas de código permanecerán casi iguales. Así que tengo un buen ejemplo.

En nuestro caso, teníamos una capa de abstracción que realmente debería haber sido dos. Todo estaba repleto en la capa de interfaz de usuario. Al dividirlo en dos capas, todo se vuelve más cohesivo, y probar y mantener las piezas individuales se vuelve mucho más simple.

No es el tamaño del código lo que molesta a tus colegas, es otra cosa. Si no pueden articularlo, intente mirar el código usted mismo como si nunca hubiera visto la implementación anterior, y evaluarlo por sus propios méritos en lugar de solo en comparación. A veces, cuando hago una refactorización larga, pierdo de vista el objetivo original y llevo las cosas demasiado lejos. Tome una mirada crítica de "panorama general" y vuelva a encaminarla, tal vez con la ayuda de un programador de pares cuyo consejo valore.

Karl Bielefeldt
fuente
1
Sí, definitivamente separa la interfaz de usuario de otras cosas, esto siempre vale la pena. Sobre su punto de perder de vista el objetivo original, estoy un poco de acuerdo, pero también podría rediseñarlo en algo mejor o en camino a mejorar. Al igual que el viejo argumento sobre la evolución ("¿de qué sirve parte de un ala?") Las cosas no mejoran si nunca se toma el tiempo para mejorarlas. No siempre sabes a dónde vas hasta que estás en el camino. Estoy de acuerdo en tratar de averiguar por qué los compañeros de trabajo están incómodos, pero tal vez realmente sea "su problema", no el suyo.
17

Me viene a la mente una cita, a menudo atribuida a Albert Einstein:

Hacer todo lo más simple posible, pero no más sencillo.

Cuando se va por la borda recortando cosas, puede hacer que el código sea más difícil de leer. Como "fácil / difícil de leer" puede ser un término muy subjetivo, explicaré exactamente lo que quiero decir con esto: una medida del grado de dificultad que tendrá un desarrollador experto para determinar "¿qué hace este código?" con solo mirar la fuente, sin la ayuda de herramientas especializadas.

Lenguajes como Java y Pascal son infames por su verbosidad. Las personas a menudo señalan ciertos elementos sintácticos y dicen burlonamente que "están ahí para facilitar el trabajo del compilador". Esto es más o menos cierto, excepto por la parte "justa". Cuanta más información explícita haya, más fácil será leer y comprender el código, no solo por un compilador sino también por un ser humano.

Si digo var x = 2 + 2;, es inmediatamente obvio que xse supone que es un número entero. Pero si digo var foo = value.Response;, está mucho menos claro qué foorepresenta o cuáles son sus propiedades y capacidades. Incluso si el compilador puede inferirlo fácilmente, pone mucho más esfuerzo cognitivo en una persona.

Recuerde que los programas deben estar escritos para que la gente los lea, y solo de manera incidental para que las máquinas los ejecuten. (Irónicamente, ¡esta cita proviene de un libro de texto dedicado a un lenguaje infame por ser extremadamente difícil de leer!) Es una buena idea eliminar cosas que son redundantes, pero no elimine el código que hace que sea más fácil para sus seres humanos averiguar qué está pasando, incluso si no es estrictamente necesario para el programa que se está escribiendo.

Mason Wheeler
fuente
77
el varejemplo no es particularmente bueno de simplificación porque la mayoría de las veces leer y comprender el código implica descubrir el comportamiento en un cierto nivel de abstracción, por lo que conocer los tipos reales de variables específicas generalmente no cambia nada (solo te ayuda a entender las abstracciones más bajas). Un mejor ejemplo sería múltiples líneas de código simple agrupadas en una sola declaración enrevesada, por ejemplo, if ((x = Foo()) != (y = Bar()) && CheckResult(x, y)) toma tiempo para asimilar, y conocer los tipos xo yno ayuda en lo más mínimo.
Ben Cottrell
15

Un código más largo puede ser más fácil de leer. Por lo general, es lo contrario, pero hay muchas excepciones, algunas de ellas descritas en otras respuestas.

Pero veamos desde un ángulo diferente. Asumimos que el nuevo código será visto como superior por los programadores más hábiles que ven las 2 piezas de código sin tener un conocimiento adicional de la cultura, la base del código o la hoja de ruta de la compañía. Incluso entonces, hay muchas razones para objetar el nuevo código. Por brevedad llamaré a "Personas que critican el nuevo código" Pecritenc :

  • Estabilidad. Si se sabía que el antiguo código era estable, se desconoce la estabilidad del nuevo código. Antes de poder utilizar el nuevo código, aún debe probarse. Si por alguna razón las pruebas adecuadas no están disponibles, el cambio es un problema bastante grande. Incluso si hay pruebas disponibles, Pecritenc puede pensar que el esfuerzo no vale la mejora (menor) del código.
  • Rendimiento / escala. El antiguo código puede haber escalado mejor, y Pecritenc supone que el rendimiento se convertirá en un problema en el futuro a medida que los clientes y las características pronto * se acumulen.
  • Extensibilidad. El antiguo código podría haber permitido una fácil introducción de algunas características que Pecritenc supone que se agregarán pronto *.
  • Familiaridad. El antiguo código puede tener patrones reutilizados que se usan en otros 5 lugares de la base de código de la compañía. Al mismo tiempo, el nuevo código utiliza un patrón elegante del que solo la mitad de la compañía ha oído hablar en este momento.
  • Lápiz labial en un cerdo. Pecritenc puede pensar que tanto el código antiguo como el nuevo son basura o irrelevantes, por lo que cualquier comparación entre ellos no tiene sentido.
  • Orgullo. Pecritenc puede haber sido el autor original del código y no le gusta que la gente haga cambios masivos a su código. Incluso podría ver las mejoras como un insulto leve, porque implican que debería haberlo hecho mejor.
Peter
fuente
44
+1 para 'Pecritenc', y un resumen muy agradable de objeciones razonables que deben considerarse previamente antes de la prefactorización.
1
Y +1 para 'extensibilidad': estaba pensando que el código original podría haber tenido funciones o clases que estaban destinadas a usarse en un proyecto futuro, por lo que las abstracciones podrían haber parecido redundantes o innecesarias, pero solo en el contexto de un solo programa.
Darren Ringer el
Además, el código en cuestión puede no ser un código crítico, por lo que se considera un desperdicio de recursos de ingeniería para limpiarlo.
Erik Eidt
@nocomprende ¿Alguna razón por la que usó prefabricado, preconsiderado y prefactorizado? ¿Método similar a Pecritenc tal vez?
Milind R
@MilindR ¿Probablemente una preconcepción, una predilección o tal vez una preferencia personal? O, tal vez simplemente sin razón alguna, una confluencia cósmica de cofactores, condiciones de conspiración confusas. Ni idea, de verdad. ¿Qué hay de tí?
1

Qué tipo de código es mejor puede depender de la experiencia de los programadores y también de las herramientas que utilizan. Por ejemplo, aquí es por qué lo que normalmente se consideraría código mal escrito puede ser más efectivo en algunas situaciones que el código orientado a objetos bien escrito que hace uso completo de la herencia:

(1) Algunos programadores simplemente no tienen una comprensión intuitiva de la programación orientada a objetos. Si su metáfora para un proyecto de software es un circuito eléctrico, entonces esperará mucha duplicación de código. Le gustará ver más o menos los mismos métodos en muchas clases. Te harán sentir como en casa. Y un proyecto en el que tiene que buscar métodos en las clases para padres o incluso en las clases de abuelos para ver qué está pasando puede parecer hostil. No desea comprender cómo funciona la clase principal y luego comprender cómo difiere la clase actual. Desea comprender directamente cómo funciona la clase actual, y encuentra confuso el hecho de que la información se extiende sobre varios archivos.

Además, cuando solo desea solucionar un problema específico en una clase específica, es posible que no le guste tener que pensar si solucionar el problema directamente en la clase base o sobrescribir el método en su clase de interés actual. (Sin herencia no tendría que tomar una decisión consciente. El valor predeterminado es simplemente ignorar problemas similares en clases similares hasta que se informen como errores). Este último aspecto no es realmente un argumento válido, aunque podría explicar algunas de las oposición.

(2) Algunos programadores usan mucho el depurador. Aunque en general estoy firmemente del lado de la herencia de código y evito la duplicación, comparto algo de la frustración que describí en (1) al depurar código orientado a objetos. Cuando sigue la ejecución del código, a veces sigue saltando entre clases (ancestro) aunque permanezca en el mismo objeto. Además, al establecer un punto de interrupción en un código bien escrito, es más probable que se active cuando no sea útil, por lo que es posible que tenga que hacer un esfuerzo para hacerlo condicional (cuando sea práctico) o incluso continuar manualmente muchas veces antes del activador relevante.


fuente
3
"clases de abuelos"! ¡Haw Haw! Solo ten cuidado con las clases de Adán y Eva. (Y la clase de Dios, por supuesto) Antes de eso, no tenía formas y estaba vacío.
1

Depende totalmente. He estado trabajando en un proyecto que no permite variables booleanas como parámetros de función, sino que requiere un dedicado enumpara cada opción.

Entonces,

enum OPTION1 { OPTION1_OFF, OPTION1_ON };
enum OPTION2 { OPTION2_OFF, OPTION2_ON };

void doSomething(OPTION1, OPTION2);

es mucho más detallado que

void doSomething(bool, bool);

Sin embargo,

doSomething(OPTION1_ON, OPTION2_OFF);

es mucho más legible que

doSomething(true, false);

El compilador debe generar el mismo código para ambos, por lo que no se gana nada utilizando el formulario más corto.

Simon Richter
fuente
0

Yo diría que la cohesión podría ser un problema.

Por ejemplo, en una aplicación web, digamos que tiene una página de administración en la que indexa todos los productos, que es esencialmente el mismo código (índice) que usaría en una situación de página de inicio, para ... simplemente indexar los productos.

Si decide parcializar todo para que pueda mantenerse SECO y elegante, tendría que agregar muchas condiciones con respecto a si el usuario que navega es un administrador o no y desordenar el código con cosas innecesarias que lo harán altamente ilegible, digamos ¡Un diseñador!

Entonces, en una situación como esta, incluso si el código es más o menos el mismo, solo porque podría escalar a otra cosa y los casos de uso podrían cambiar ligeramente, sería malo ir tras cada uno de ellos agregando condiciones e ifs. Por lo tanto, una buena estrategia sería deshacerse del concepto DRY y romper el código en partes que se puedan mantener.

frcake
fuente
0
  • Cuando menos código no hace el mismo trabajo que más código. Refactorizar por simplicidad es bueno, pero debe tener cuidado de no simplificar demasiado el espacio del problema con el que se encuentra esta solución. 980 líneas de código podrían manejar más casos de esquina que 450.
  • Cuando menos código no falla tan bien como más código. He visto un par de trabajos de "ref *** toring" realizados en el código para eliminar el try-catch "innecesario" y otro manejo de casos de error. El resultado inevitable fue que, en lugar de mostrar un cuadro de diálogo con un mensaje agradable sobre el error y lo que el usuario podía hacer, la aplicación se bloqueó o YSOD.
  • Cuando menos código es menos mantenible / extensible que más código. Refactorizar la concisión del código a menudo elimina las construcciones de código "innecesarias" en interés de LoC. El problema es que esas construcciones de código, como las declaraciones de interfaz paralela, los métodos / subclases extraídos, etc. son necesarios si este código alguna vez necesita hacer más de lo que hace actualmente, o hacerlo de manera diferente. En el extremo, ciertas soluciones personalizadas para el problema específico pueden no funcionar si la definición del problema cambia solo un poco.

    Un ejemplo; Tienes una lista de enteros. Cada uno de estos enteros tiene un valor duplicado en la lista, excepto uno. Su algoritmo debe encontrar ese valor no emparejado. La solución de caso general es comparar cada número con cualquier otro número hasta que encuentre un número que no tenga engaño en la lista, que es una operación N ^ 2 veces. También podría construir un histograma usando una tabla hash, pero eso es muy poco eficiente en cuanto al espacio. Sin embargo, puede hacerlo de tiempo lineal y espacio constante utilizando una operación XOR a nivel de bits; XOR cada entero contra un "total" en ejecución (comenzando con cero), y al final, la suma de ejecución será el valor de su entero sin emparejar. Muy elegante. Hasta que los requisitos cambien, y más de un número en la lista no pueda ser emparejado, o los enteros incluyan cero. Ahora su programa devuelve basura o resultados ambiguos (si devuelve cero, ¿eso significa que todos los elementos están emparejados o que el elemento no emparejado es cero?). Tal es el problema de las implementaciones "inteligentes" en la programación del mundo real.

  • Cuando menos código es menos autodocumentado que más código. Poder leer el código en sí mismo y determinar qué está haciendo es fundamental para el desarrollo del equipo. Dar un algoritmo brain-f *** que escribiste que funciona maravillosamente para un desarrollador junior y pedirle que lo modifique para modificar ligeramente la salida no te llevará muy lejos. Muchos desarrolladores senior también tendrían problemas con esa situación. Poder comprender en cualquier momento qué está haciendo el código y qué podría salir mal, es clave para un entorno de desarrollo de trabajo en equipo (e incluso solo; te garantizo que el destello de genio que tenías cuando escribiste un 5 el método de línea para curar el cáncer desaparecerá cuando regrese a esa función buscando hacer que también cure el Parkinson).
KeithS
fuente
0

El código de computadora necesita hacer varias cosas. Un código "minimalista" que no hace estas cosas no es un buen código.

Por ejemplo, un programa de computadora debe cubrir todos los casos posibles (o como mínimo, todos los casos probables). Si un código cubre solo un "caso base" e ignora otros, no es un buen código, incluso si es breve.

El código de la computadora debe ser "escalable". Un código críptico puede funcionar solo para una aplicación especializada, mientras que un programa más largo pero más abierto puede facilitar la adición de nuevas aplicaciones.

El código de la computadora debe ser claro. Como demostró otro respondedor, es posible que un codificador de núcleo duro produzca una función de tipo "algorítmico" de una línea que haga el trabajo. Pero la frase tenía que dividirse en cinco "oraciones" diferentes antes de que fuera claro para el programador promedio.

Tom Au
fuente
El deber está en el ojo del espectador.
-2

Rendimiento computacional. Al optimizar el revestimiento de tuberías o ejecutar partes de su código en paralelo, puede ser beneficioso, por ejemplo, no realizar un bucle de 1 a 400, sino de 1 a 50 y colocar 8 instancias de código similar en cada bucle. No estoy asumiendo que este fuera el caso en su situación, pero es un ejemplo en el que más líneas son mejores (en términos de rendimiento).

Hans Janssen
fuente
44
Un buen compilador debería saber mejor que un programador promedio cómo desenrollar bucles para una arquitectura de computadora específica, pero el punto general es válido. Una vez miré el código fuente de una rutina de multiplicación de matrices de una biblioteca de alto rendimiento de Cray. La multiplicación de matrices consta de tres bucles anidados y unas 6 líneas de código en total, ¿verdad? Incorrecto: la rutina de la biblioteca funcionó con aproximadamente 1100 líneas de código, ¡más un número similar de líneas de comentarios que explican por qué fue tan largo!
alephzero
1
@alephzero wow, me encantaría ver ese código, debe ser solo Cray Cray.
@alephzero, los buenos compiladores pueden hacer mucho, pero lamentablemente no todo. El lado positivo es que esas son las cosas que mantienen la programación interesante.
Hans Janssen
2
@alephzero De hecho, un buen código de multiplicación de matrices no solo ahorra un poco de tiempo (es decir, lo reduce en un factor constante), sino que utiliza un algoritmo totalmente diferente con diferente complejidad asintótica, por ejemplo, el algoritmo de Strassen es aproximadamente O (n ^ 2.8) en lugar de O (n ^ 3).
Arthur Tacca