function - tipos - ¿Cuándo es una función demasiado larga?
tipos de funciones (24)
60 líneas es grande pero no demasiado larga para una función. Si cabe en una pantalla en un editor, puedes verlo todo de una vez. Realmente depende de lo que estén haciendo las funciones.
Por qué puedo romper una función:
- Es muy largo
- Hace que el código sea más fácil de mantener dividiéndolo y usando nombres significativos para la nueva función
- La función no es cohesiva
- Partes de la función son útiles en sí mismas.
- Cuando es difícil encontrar un nombre significativo para la función (Probablemente esté haciendo demasiado)
35 líneas, 55 líneas, 100 líneas, 300 líneas? ¿Cuándo deberías comenzar a separarlo? Pregunto porque tengo una función con 60 líneas (incluyendo comentarios) y estaba pensando en separarla.
long_function(){ ... }
dentro:
small_function_1(){...}
small_function_2(){...}
small_function_3(){...}
Las funciones no se usarán fuera de la función long, haciendo que funciones más pequeñas signifiquen más llamadas de función, etc.
¿Cuándo separarías una función en otras más pequeñas? ¿Por qué?
- Los métodos deben hacer solo una cosa lógica (pensar en la funcionalidad)
- Deberías poder explicar el método en una sola oración
- Debe caber en la altura de su pantalla
- Evite gastos generales innecesarios (comentarios que señalan lo obvio ...)
- La prueba unitaria es más fácil para pequeñas funciones lógicas
- Verifique si parte de la función puede ser reutilizada por otras clases o métodos
- Evite el acoplamiento excesivo entre clases
- Evite las estructuras de control profundamente anidadas
Gracias a todos por las respuestas , edite la lista y vote por la respuesta correcta; la elegiré;)
Estoy refaccionando ahora con esas ideas en mente :)
Al extender el espíritu de un tweet del tío Bob hace un tiempo, sabes que una función se está haciendo demasiado larga cuando sientes la necesidad de poner una línea en blanco entre dos líneas de código. La idea es que si necesita una línea en blanco para separar el código, su responsabilidad y alcance se separan en ese punto.
Aquí hay una lista de banderas rojas (sin ningún orden en particular) que podría indicar que una función es demasiado larga:
Estructuras de control profundamente anidadas : por ejemplo, for-loops 3 niveles profundos o incluso solo 2 niveles de profundidad con declaraciones if anidadas que tienen condiciones complejas.
Demasiados parámetros que definen el estado : por parámetro que define el estado , me refiero a un parámetro de función que garantiza un camino de ejecución particular a través de la función. Obtenga demasiados de este tipo de parámetros y tenga una explosión combinatoria de rutas de ejecución (esto generalmente ocurre en conjunto con el n. ° 1).
Lógica que se duplica en otros métodos : la reutilización del código deficiente es un gran contribuyente al código de procedimiento monolítico. Muchas de estas duplicaciones lógicas pueden ser muy sutiles, pero una vez redimensionadas, el resultado final puede ser un diseño mucho más elegante.
Acoplamiento excesivo entre clases : esta falta de encapsulación adecuada da como resultado que las funciones se ocupen de las características íntimas de otras clases y, por lo tanto, las alarguen.
Sobrecarga innecesaria : los comentarios que señalan las clases obvias, profundamente anidadas, los captadores y establecedores superfluos para las variables de clase anidadas privadas, y los nombres de función / variable inusualmente largos pueden crear ruido sintáctico dentro de funciones relacionadas que finalmente aumentarán su longitud.
Su enorme pantalla de nivel desarrollador no es lo suficientemente grande como para mostrarla : en realidad, las pantallas actuales son lo suficientemente grandes como para que una función que esté cerca de su altura sea probablemente demasiado larga. Pero, si es más grande , esta es una pistola humeante que algo está mal.
No puede determinar de inmediato el propósito de la función : además, una vez que realmente determina su propósito, si no puede resumir este propósito en una sola oración o si tiene un tremendo dolor de cabeza, esto debería ser una pista.
En conclusión, las funciones monolíticas pueden tener consecuencias de gran alcance y, a menudo, son un síntoma de importantes deficiencias de diseño. Cada vez que encuentro un código que es una alegría absoluta de leer, su elegancia es inmediatamente evidente. Y adivina qué: las funciones a menudo son muy cortas.
Calcule aproximadamente el tamaño de su pantalla (así que vaya a obtener una gran pantalla panorámica pivote y gírela) ... :-)
Broma aparte, una cosa lógica por función.
Y lo positivo es que las pruebas unitarias son mucho más fáciles de hacer con pequeñas funciones lógicas que hacen 1 cosa. ¡Las funciones grandes que hacen muchas cosas son más difíciles de verificar!
/ Johan
Creo que hay una gran advertencia sobre el mantra de "hacer una sola cosa" en esta página. A veces, hacer una cosa hace malabares con muchas variables. No rompa una función larga en una serie de funciones más pequeñas si las funciones más pequeñas terminan teniendo largas listas de parámetros. Hacer eso solo convierte una sola función en un conjunto de funciones altamente acopladas sin ningún valor individual real.
Eche un vistazo al ciclomático de McCabe, en el cual divide su código en un gráfico donde, "Cada nodo en el gráfico corresponde a un bloque de código en el programa donde el flujo es secuencial y los arcos corresponden a las ramas tomadas en el programa. "
Ahora imagine que su código no tiene funciones / métodos; es solo una gran expansión de código en forma de gráfico.
Quieres romper esta expansión en métodos. Considere que, cuando lo haga, habrá una cierta cantidad de bloques en cada método. Solo un bloque de cada método será visible para todos los demás métodos: el primer bloque (estamos suponiendo que podrá saltar a un método en un solo punto: el primer bloque). Todos los demás bloques en cada método serán información oculta dentro de ese método, pero cada bloque dentro de un método puede saltar a cualquier otro bloque dentro de ese método.
Para determinar qué tamaño deben tener tus métodos en términos de número de bloques por método, una pregunta que debes hacerte es: ¿cuántos métodos debo tener para minimizar el número potencial máximo de dependencias (MPE) entre todos los bloques?
Esa respuesta viene dada por una ecuación. Si r es la cantidad de métodos que minimiza el MPE del sistema, y n es el número de bloques en el sistema, entonces la ecuación es: r = sqrt (n)
Y se puede demostrar que esto le da a la cantidad de bloques por método ser, también, sqrt (n).
En mi opinión, la respuesta es: cuando hace demasiadas cosas. Su función debe realizar solo las acciones que espera del nombre de la función en sí. Otra cosa a considerar es si desea reutilizar algunas partes de sus funciones en otros; en este caso, podría ser útil dividirlo.
Esto es, en parte, una cuestión de gusto, pero la forma en que lo determino es que trato de mantener mis funciones aproximadamente solo mientras que quepan en mi pantalla al mismo tiempo (como máximo). La razón es que es más fácil entender lo que sucede si puedes ver todo al mismo tiempo.
Cuando codigo, es una mezcla de escribir funciones largas, luego refactorizar para extraer bits que podrían ser reutilizados por otras funciones, y escribir pequeñas funciones que hagan tareas discretas a medida que avanzo.
No sé si hay una respuesta correcta o incorrecta a esto (por ejemplo, puede establecer 67 líneas como su máximo, pero puede haber ocasiones en las que tenga sentido agregar algunas más).
Estoy de acuerdo en que una función solo debería hacer una cosa, pero en qué nivel es esa una cosa.
Si sus 60 líneas están logrando una cosa (desde la perspectiva de su programa) y las piezas que componen esas 60 líneas no van a ser utilizadas por otra cosa, entonces 60 líneas está bien.
No hay ningún beneficio real para romperlo, a menos que pueda dividirlo en piezas concretas que se sostengan por sí mismas. La métrica a usar es funcionalidad y no líneas de código.
He trabajado en muchos programas en los que los autores llevaron la única cosa a un nivel extremo y todo lo que hizo fue hacer que pareciera que alguien tomó una granada para una función / método y la hizo explotar en docenas de piezas inconexas que son difícil de seguir.
Al extraer piezas de esa función, también debe considerar si va a agregar cualquier sobrecarga innecesaria y evitar pasar grandes cantidades de datos.
Creo que el punto clave es buscar la reutilización en esa función larga y sacar esas partes. Lo que te queda es la función, ya sea de 10, 20 o 60 líneas.
He escrito 500 funciones de línea antes, sin embargo, estas fueron simplemente grandes declaraciones de cambio para decodificar y responder mensajes. Cuando el código para un solo mensaje se volvió más complejo que un solo if-then-else, lo extraje.
En esencia, aunque la función era de 500 líneas, las regiones mantenidas independientemente tenían un promedio de 5 líneas.
La razón principal por la que generalmente rompo una función es porque partes de ella también son ingredientes en otra función cercana que estoy escribiendo, por lo que las partes comunes se excluyen. Además, si usa muchos campos o propiedades de otra clase, existe una buena posibilidad de que el fragmento relevante se pueda eliminar al por mayor y, si es posible, pasar a la otra clase.
Si tiene un bloque de código con un comentario en la parte superior, considere extraerlo en una función, con los nombres de función y argumento que ilustran su propósito, y reservar el comentario para la lógica del código.
¿Estás seguro de que no hay piezas allí que serían útiles en otros lugares? ¿Qué tipo de función es?
Mi heurística personal es que es demasiado larga si no puedo ver todo sin desplazarme.
Mi idea es que si tengo que preguntarme si es demasiado largo, probablemente sea demasiado largo. Ayuda a hacer funciones más pequeñas, en esta área, porque podría ayudar más adelante en el ciclo de vida de la aplicación.
No hay reglas reales duras y rápidas para eso. En general, me gusta que mis métodos simplemente "hagan una cosa". Entonces, si está tomando datos, luego haciendo algo con esos datos, luego escribiéndolos en un disco, entonces dividiré el acaparamiento y la escritura en métodos separados, así que mi método "principal" solo contiene el "hacer algo".
Sin embargo, ese "hacer algo" podría ser un buen número de líneas, por lo que no estoy seguro de que un número de líneas sea la medida correcta para usar :)
Editar: Esta es una sola línea de código que envié por correo el trabajo la semana pasada (para demostrar un punto ... no es algo que tenga un hábito :)) - ciertamente no quisiera 50-60 de estos chicos malos en mi método :RE
return level4 != null ? GetResources().Where(r => (r.Level2 == (int)level2) && (r.Level3 == (int)level3) && (r.Level4 == (int)level4)).ToList() : level3 != null ? GetResources().Where(r => (r.Level2 == (int)level2) && (r.Level3 == (int)level3)).ToList() : level2 != null ? GetResources().Where(r => (r.Level2 == (int)level2)).ToList() : GetAllResourceList();
Normalmente rompo las funciones por la necesidad de colocar comentarios que describan el siguiente bloque de código. Lo que previamente entraba en los comentarios ahora va al nuevo nombre de la función. Esta no es una regla difícil, pero (para mí) es una buena regla general. Me gusta el código que habla por sí mismo mejor que uno que necesita comentarios (ya que he aprendido que los comentarios generalmente mienten)
Normalmente utilizo un enfoque basado en pruebas para escribir código. En este enfoque, el tamaño de la función a menudo está relacionado con la granularidad de sus pruebas.
Si tu prueba está lo suficientemente enfocada, te llevará a escribir una pequeña función enfocada para que pase la prueba.
Esto también funciona en la otra dirección. Las funciones deben ser lo suficientemente pequeñas para probar de manera efectiva. Entonces, cuando trabajo con código heredado, a menudo encuentro que desgloso funciones más grandes para probar las diferentes partes de ellos.
Normalmente me pregunto "¿cuál es la responsabilidad de esta función?" Y si no puedo expresar la responsabilidad en una oración clara y concisa, y luego traducir eso en una pequeña prueba enfocada, me pregunto si la función es demasiado grande.
Regla general: si una función contiene bloques de código que hacen algo, que está algo separado del resto del código, colóquelo en una función separada. Ejemplo:
function build_address_list_for_zip($zip) {
$query = "SELECT * FROM ADDRESS WHERE zip = $zip";
$results = perform_query($query);
$addresses = array();
while ($address = fetch_query_result($results)) {
$addresses[] = $address;
}
// now create a nice looking list of
// addresses for the user
return $html_content;
}
mucho más bonito:
function fetch_addresses_for_zip($zip) {
$query = "SELECT * FROM ADDRESS WHERE zip = $zip";
$results = perform_query($query);
$addresses = array();
while ($address = fetch_query_result($results)) {
$addresses[] = $address;
}
return $addresses;
}
function build_address_list_for_zip($zip) {
$addresses = fetch_addresses_for_zip($zip);
// now create a nice looking list of
// addresses for the user
return $html_content;
}
Este enfoque tiene dos ventajas:
Siempre que necesite buscar direcciones para un determinado código postal, puede usar la función disponible.
Cuando necesite leer nuevamente la función
build_address_list_for_zip()
, sabrá qué hará el primer bloque de código (busca direcciones para un determinado código postal, al menos eso es lo que puede derivar del nombre de la función). Si hubiera dejado el código de consulta en línea, primero tendría que analizar ese código.
[Por otro lado (negaré que te dije esto, incluso bajo tortura): si lees mucho sobre la optimización de PHP, podrías tener la idea de mantener la cantidad de funciones lo más pequeña posible, porque la función de llamada es muy, muy caro en PHP. No sé nada de eso ya que nunca hice ningún punto de referencia. Si ese es el caso, es mejor que no sigas ninguna de las respuestas a tu pregunta si tu aplicación es muy "sensible al rendimiento" ;-)]
Se han llevado a cabo algunas investigaciones exhaustivas sobre este tema, si quieres la menor cantidad de errores, tu código no debería ser demasiado largo. Pero tampoco debería ser demasiado corto.
No estoy de acuerdo con que un método se ajuste a su pantalla en uno, pero si se desplaza hacia abajo en más de una página, el método es demasiado largo.
Vea El tamaño óptimo de clase para software orientado a objetos para mayor discusión.
Si tiene más de tres ramas, generalmente esto significa que una función o método debe dividirse, para encapsular la lógica de bifurcación en diferentes métodos. Cada bucle for, if statement, etc. no se ve como una bifurcación en el método de llamada. El código de Cobertura para Java (y estoy seguro de que hay otras herramientas para otros idiomas) calcula el número de if, etc. en una función para cada función y lo suma para la "complejidad ciclomática promedio". Si una función / método solo tiene tres ramas, obtendrá un tres en esa métrica, lo cual es muy bueno. A veces es difícil seguir esta guía, concretamente para validar la entrada del usuario, sin embargo, poner ramas en diferentes métodos ayuda no solo al desarrollo y mantenimiento sino también a las pruebas, ya que las entradas a los métodos que realizan la bifurcación se pueden analizar fácilmente para ver qué entradas deben agregarse a los casos de prueba para cubrir las ramas que no estaban cubiertas; si todas las ramas estuvieran dentro de un único método, las entradas deberían rastrearse desde el inicio del método, lo que dificulta la capacidad de prueba.
Sospecho que encontrarás muchas respuestas sobre esto.
Probablemente lo dividiría según las tareas lógicas que se realizaban dentro de la función. Si le parece que su cuento se está convirtiendo en una novela, le sugiero que busque y extraiga distintos pasos.
Por ejemplo, si tiene una función que maneja algún tipo de entrada de cadena y devuelve un resultado de cadena, puede interrumpir la función según la lógica para dividir la cadena en partes, la lógica para agregar caracteres adicionales y la lógica para ponerlo todos juntos de nuevo como un resultado formateado.
En resumen, cualquier cosa que haga que su código esté limpio y sea fácil de leer (ya sea simplemente asegurando que su función tenga buenos comentarios o descomponerlo) es el mejor enfoque.
Tenga en cuenta que puede terminar refaccionando solo por cambiar el factor, potencialmente haciendo que el código sea más ilegible de lo que era en primer lugar.
¡Un ex colega mío tenía una regla extraña que una función / método debe contener solo 4 líneas de código! Trató de apegarse a esto tan rígidamente que los nombres de sus métodos a menudo se volvieron repetitivos y sin sentido, además de que las llamadas se volvieron anidadas y confusas.
Entonces mi propio mantra se ha convertido: si no puedes pensar en un nombre de función / método decente para el fragmento de código que estás rediseñando, no te molestes.
Una cosa (y esa cosa debería ser obvia desde el nombre de la función), pero no más que una pantalla llena de código, independientemente. Y siéntase libre de aumentar el tamaño de su fuente. Y en caso de duda, refactorice en dos o más funciones.
Una función solo debe hacer una cosa. Si está haciendo muchas cosas pequeñas en una función, haga que cada cosa pequeña sea una función y llame a esas funciones desde la función larga.
Lo que realmente no quieres hacer es copiar y pegar cada 10 líneas de tu función larga en funciones cortas (como tu ejemplo sugiere).
suponiendo que esté haciendo una cosa, la duración dependerá de:
- qué estás haciendo
- qué idioma estás usando
- ¿Cuántos niveles de abstracción necesitas manejar en el código?
60 líneas pueden ser demasiado largas o pueden ser las correctas. Sospecho que puede ser demasiado largo sin embargo.