¿Es mejor escribir varias funciones más pequeñas o una función más grande para realizar cierta tarea?

No existe una regla general exacta que pueda expresarse en líneas de código, por supuesto, pero las cosas pueden aclararse una vez que comprenda qué es la complejidad ciclomática.

Como el artículo anterior indica correctamente, hay pruebas suficientes para afirmar que existe una correlación positiva entre una serie de defectos que se pueden observar en un programa de computadora y su complejidad ciclomática. Cuando lo piensa, intuitivamente, tiene mucho sentido: los humanos que escriben programas tienen capacidades limitadas para avanzar mentalmente a través de todos los flujos de ejecución, pensar en todos los invariantes, precondiciones y postcondiciones, a medida que crece la complejidad. Solo imagine el mundo antes de la programación procesal como un caso extremo. Ahora, como sabemos que no tratamos muy bien la complejidad y que nuestros programas se encuentran con errores, tiene sentido reducirla.

Entonces, ¿cómo ayudan las funciones en la reducción de la complejidad ciclomática? Puede pensar que, formalmente, no lo hacen, porque el programa pasa por los mismos cambios de estado, independientemente de si ocurren dentro de una función o dentro de un cuerpo de programa ‘principal’. En un nivel muy bajo, ¿qué son las funciones sino las regiones de memoria a las que el compilador querrá entrar y salir, verdad? Pues mal. Al extraer ciertas tareas en funciones, hace que sus clientes se preocupen solo por sus precondiciones y postcondiciones, y un conjunto reducido de estados bien definidos que la función promete producir. Puede pensar de manera informal como una ‘función invariante’. Entonces, ahora que tenemos una invariante o una ilusión, la complejidad ciclomática del programa principal se reduce efectivamente, porque parte de ella se ha movido a una función bien definida que toma entradas y las convierte previsiblemente en salidas.

Teniendo esto en cuenta, la respuesta a su pregunta es: sí, sin duda es mejor tener una función más pequeña que una función grande. ¿Dónde deben hacerse los cortes? Bueno, donde ve porciones de código que pueden identificarse correctamente como realizando una determinada tarea, tomando algunas entradas y produciendo algunas salidas sin producir directamente ningún otro efecto secundario y cambiando el estado de su programa (excepto algún estado local), estos se ven como un Buena función de candidatos. Aún mejor, si parecen ser funciones puras y sin estado, como funciones matemáticas. O, si imponen la encapsulación al abstraer cosas como el acceso y el procesamiento de datos. Como con todo, puede exagerar y hacer que su programa sea efectivamente ilegible al obligar al lector a saltar de una función a otra en caso de que sean irrazonablemente pequeños.

En cuanto al rendimiento, no hace ninguna diferencia. Vamos a sacar eso del camino. Los compiladores tratan las funciones más pequeñas como parte del código más grande a menos que haya una razón sólida para no hacerlo. Incluso sin alineación, la sobrecarga de la llamada de función está a escala microscópica. Tampoco debería ser muy diferente en los idiomas interpretados.

Escribir funciones más pequeñas es más trabajo cuando se escribe código inicialmente. Debido a eso, escribir una función más larga y refactorizarla en funciones más pequeñas en el curso del desarrollo suele ser más fácil y tiene menos trabajo preliminar.

Mantener grandes funciones es solo una pesadilla. Son propensos a tener más complejidad ciclomática, mucho más difíciles de entender, incluso después de un par de semanas, y es más difícil escribir pruebas unitarias.

También creo firmemente en la filosofía del “código es un comentario”. No significa que nunca debas escribir comentarios. Significa que solo debe escribir comentarios cuando el código en sí mismo no describe lo que hace. Refactorizar su función en partes más pequeñas brinda la oportunidad de nombrar sus bloques de código con nombres de funciones en lugar de comentarios. Entonces, menos trabajo para describir el código.

Las funciones más pequeñas proporcionan más flexibilidad para la reutilización del código. Con muchas funciones pequeñas, puede beneficiarse de la reutilización de partes de un código.

Sin embargo, es difícil cuantificar una regla para exagerar. Obviamente, convertir ” if (a==b) doSomething() ” a ” doSomethingOnlyWhenTwoValuesEqual(a, b) ” es menos legible y menos mantenible. No lo lleves tan lejos como reinventar la rueda. Eche un vistazo al código revisado en proyectos de código abierto bien conocidos para tener una idea del mismo. Algunos proyectos incluso tienen reglas para tamaños máximos de funciones. Échales un vistazo.

Esto es altamente deseable.

Realmente no hay un tamaño mínimo. A menudo extraigo un revestimiento en su propia llamada al método. Lo hago en primer lugar porque esa línea no debe repetirse si tiene alguna sustancia. Lo hago en segundo lugar porque me da la oportunidad de decidir cómo quiero que se vea esa línea dentro del método que la usa. Le doy un buen nombre de método (si hubo un comentario sobre la línea, entonces con un poco de redacción, el texto en el comentario podría convertirse en el nombre del método).

En cuanto a las líneas máximas, implica un cierto juicio. Personalmente, creo que me está yendo bastante bien si la mayoría de mis métodos tienen menos de 10 líneas, pero no invierto una gran cantidad de tiempo en crear mi código la primera vez. Intento mantenerlo organizado, luego procedo a ordenar un poco cada vez que vuelvo al código para algo nuevo.

Para obtener más información, consulte mi respuesta reciente a una pregunta similar: la respuesta de Ryan Cook a ¿Es siempre mejor factorizar mi código en bloques funcionales más pequeños hasta que haya una única responsabilidad para cada función?

En la escuela nos enseñaron que todo lo que no cabe en una hoja de papel debe dividirse en funciones … Por supuesto, no he impreso código en papel en al menos diez años.

Mis reglas generales:

1: legibilidad, legibilidad, legibilidad
2: modularidad

– Si tiene un interruptor o está anidado si tiene mucho código en cada rama, realmente considere hacer funciones solo para facilitar la lectura.
– Si se siente tentado a hacer un gran comentario, eso probablemente indica que debería ser una función.
– Si más de un lugar va a hacer algo similar, haga una función para hacerlo. Solo tiene que escribirlo y depurarlo una vez.
– Si aún no sabe cómo hacerlo, hágalo una función. “trozos”. Por ahora, solo haga que devuelva un valor ficticio razonable. Esto obtendrá su código medio escrito para compilar y ejecutar.

Nunca está de más hacer una función. La gente hablará de “sobrecarga de funciones” (los ciclos de procesador adicionales para configurar, llamar y regresar de una función), pero son realmente mínimos para un procesador con suficientes registros. Siempre puede “en línea” una función. Eso le brinda las ventajas de legibilidad al tiempo que elimina cualquier sobrecarga adicional (algunos compiladores pueden hacer esto de manera predeterminada para funciones pequeñas).

Yo diría que dividirlos en pasos lógicos más pequeños generalmente es una mejor manera de hacerlo. Hace que sea más fácil escribir pruebas unitarias para cada parte de su programa, lo que a su vez facilita la depuración cuando puede reducir la fuente de cualquier problema mucho más rápido. También significa que si encuentra una manera más eficiente de hacer una determinada parte del proceso, es mucho más fácil “intercambiar” la función que maneja esa parte del programa. Además, el código es más fácil de mantener, ya que dividirlo en funciones más pequeñas generalmente mejora la legibilidad.

Debes apuntar a cualquier tamaño que sea necesario.

El tamaño de una función no es relevante en sí mismo; Si tengo que codificar un conjunto de reglas de negocio particularmente complicado que no tiene sentido romper (sucede, aunque muy raramente), puedo escribir y escribiré más de 100 funciones de línea.

Siempre que cumpla con las reglas básicas del sentido común (refactorizar la funcionalidad repetida en ayudantes, apegarse a niveles de sangría relativamente bajos, nombres transparentes), la pérdida de legibilidad por tener una función larga es insignificante.

También me gusta limitar el alcance de las cosas en funciones más largas (el código es pseudo-C #) y usar funciones anónimas / funciones de primera clase para funciones auxiliares, para no contaminar la clase:

Exportación pública vacía (string filename)
{
#region Helpers
var WriteToCell = (x, y, val) => {algún conjuro arcano que no recuerdo fuera de mi cabeza ‘};
#endregion

#region Inicializar / recopilar datos
{
int cuenta;
// Hacer cosas
}
#endregion

#region Configurar plantilla de Excel
{
int cuenta; // Se puede reutilizar de forma segura y sana porque tiene un alcance limitado.
// Haz más cosas
}
#endregion

#region Bucle de exportación principal
{
// Haz cosas importantes
}
#endregion
}

Realmente no tiene sentido en mi mente factorizar ninguno de esos bloques en una función separada (incluso si están lo suficientemente desacoplados), ya que nunca se reutilizan; Gracias al soporte de IDE, puedo colapsar cada región separada y solo mirar lo que me interesa. Aunque el método en sí es bastante grande, el alcance real de cualquier dato particular que esté viendo es limitado, lo que hace que cada # región sea digerible.

Limitar el alcance de esa manera es más importante que el tamaño de la función, limitar el tamaño de la función es solo una forma de lograrlo. La razón por la cual las variables globales están mal vistas es porque su alcance es enorme . Es imposible mantener todos los lugares donde se usa en su cabeza en algún momento (por supuesto, puede usar las herramientas IDE para averiguar dónde se usa, pero esa es la diferencia entre leer desde la RAM si lo sabe desde la parte superior de la pantalla). cabeza, y leer desde el disco si tiene que buscarlo).

Por lo tanto: Sí, es una buena idea minimizar el tamaño de los métodos / funciones, pero tenga en cuenta por qué lo está haciendo y por qué ayuda a la legibilidad. De esa manera, sabes cuándo y cómo romper esa regla.

Su función debe realizar una sola tarea. Idealmente, si su función tiene una tarea intermedia, debe refactorizarla en una subrutina separada, a veces, cuando sabe que es una de las cosas, puede deslizarse. Sin embargo, no hay un tamaño deseable.

Depende de muchos factores, no hay una regla universal. Por ejemplo, si configura una llamada a una función (que no está en línea), el compilador y, además, la propia CPU pueden no optimizar aún más este código con respecto a su código circundante. La CPU generalmente no puede realizar ninguna ejecución especulativa para mezclar la llamada y el código llamado, por lo que sus recursos internos no se utilizarán lo suficiente (sin embargo, probablemente serán utilizados por otro hilo). Por otro lado, una función puede asignar mejor los registros internos de la CPU sin almacenar variables en la pila [math] – [/ math] incluso si estos valores no salen del núcleo (ir a una memoria caché o memoria que es costosa), solo los procesadores de gama alta podrían “cambiarles el nombre” internamente a su archivo de registro interno (aunque supongo). Sin embargo, depende en gran medida del compilador, la plataforma …

Uno de los objetivos principales debe ser la claridad de los datos y el flujo del programa [matemática] – [/ matemática] para programas de tamaño incluso modesto y complejidad algorítmica, debe apuntar a ello en primer lugar (o se arrepentirá más tarde al depurar y / o reutilizando el código).

Como de costumbre, una forma en el medio podría ser mejor. En mi experiencia, necesita dividir su biblioteca de funciones en dos partes:

  1. Lo que llamo “funciones de servicio”. Un ejemplo de esto es abrir un archivo. Cuando abre un archivo, hay algunas comprobaciones previas y algunas acciones que deben tomarse en función de la información recibida. Esta es una tarea general realizada con frecuencia (es decir, no función o posición específica) y generalmente se puede programar con un código muy pequeño. Todas esas tareas generales deben ser pequeñas (al menos más pequeñas que otras) y deben mantenerse en una ubicación compartida, por lo que no tendrá que volver a escribir o peor aún copiar y mantener la misma funcionalidad en varias ocasiones / lugares.
  2. Lo que yo llamo “detalles específicos del deber”: son funciones que realizan tareas específicas del programa, probablemente tareas únicas. Dichas funciones tienden a ser más grandes que el código de propósito general. Esto es normal y no es un problema. Lo único que debe tener en cuenta es que puede tener algunos problemas comunes en un entorno específico del problema, evitando así volver a escribir el mismo código en diferentes funciones …