¿Son aceptables las funciones largas si tienen una estructura interna?

23

Cuando trato con algoritmos complicados en lenguajes con soporte para funciones anidadas (como Python y D), a menudo escribo funciones enormes (porque el algoritmo es complicado) pero mitigo esto usando funciones anidadas para estructurar el código complicado. ¿Las funciones enormes (más de 100 líneas) todavía se consideran malas, incluso si están bien estructuradas internamente mediante el uso de funciones anidadas?

Editar: para aquellos de ustedes que no están familiarizados con Python o D, las funciones anidadas en estos lenguajes también permiten el acceso al alcance de la función externa. En D este acceso permite la mutación de variables en el ámbito externo. En Python solo permite la lectura. En D puede deshabilitar explícitamente el acceso al ámbito externo en una función anidada al declararlo static.

dsimcha
fuente
55
Regularmente escribo más de 400 funciones / métodos de línea. Tengo que poner esa declaración de cambio de 500 casos en alguna parte :)
Callum Rogers
77
@Callum Rogers: en Python, en lugar de tener una declaración de cambio de 500 mayúsculas, usaría un diccionario / mapeo de búsqueda. Creo que son superiores a tener más o menos 500 declaraciones de cambio de caso. Aún necesita 500 líneas de definiciones de diccionario (alternativamente, en Python, puede usar la reflexión para enumerarlas dinámicamente), pero la definición de diccionario es datos, no código; y hay mucho menos reparos acerca de tener una definición de datos grande que una definición de función grande. Además, los datos son más robustos que el código.
Lie Ryan
Me estremezco cada vez que veo funciones con mucha LOC, me hace preguntarme cuál es el propósito de la modularidad. Es más fácil seguir la lógica y depurar con funciones más pequeñas.
Aditya P
El antiguo y familiar lenguaje Pascal también permite el acceso al alcance externo, y también lo hace la extensión de función anidada en GNU C. (Sin embargo, estos lenguajes solo permiten funargs descendentes: es decir, los argumentos que son funciones que llevan alcance, solo pueden ser transmitido y no devuelto, lo que requeriría un soporte de cierre léxico completo).
Kaz
Un buen ejemplo de funciones que tienden a ser largas son los combinadores que escribe cuando realiza la programación reactiva. Llegan fácilmente a treinta líneas, pero se duplicarán en tamaño si se dividen porque pierde cierres.
Craig Gidney

Respuestas:

21

¡Siempre recuerda la regla, una función hace una cosa y lo hace bien! Si puede hacerlo, evite las funciones anidadas.

Se dificulta la legibilidad y las pruebas.

Bryan Harrington
fuente
99
Siempre he odiado esta regla porque una función que hace "una cosa" cuando se ve en un alto nivel de abstracción puede hacer "muchas cosas" cuando se ve en un nivel inferior. Si se aplica incorrectamente, la regla de "una cosa" puede conducir a API excesivamente finas.
dsimcha
1
@dsimcha: ¿Qué regla no se puede aplicar incorrectamente y generar problemas? Cuando alguien está aplicando "reglas" / tópicos más allá del punto en que son útiles, simplemente resalte otro: todas las generalizaciones son falsas.
2
@Roger: una queja legítima, excepto que, en mi humilde opinión, la regla a menudo se aplica incorrectamente en la práctica para justificar API excesivamente finas y de ingeniería excesiva. Esto tiene costos concretos en el sentido de que la API se vuelve más difícil y más detallada de usar para casos de uso simples y comunes.
dsimcha
2
"Sí, la regla está mal aplicada, ¡pero la regla está mal aplicada demasiado!" : P
1
Mi función hace una cosa y lo hace muy bien: a saber, ocupar 27 pantallas. :)
Kaz
12

Algunos han argumentado que las funciones cortas pueden ser más propensas a errores que las funciones largas .

Card y Glass (1990) señalan que la complejidad del diseño realmente involucra dos aspectos: la complejidad dentro de cada componente y la complejidad de las relaciones entre los componentes.

Personalmente, descubrí que el código de línea recta bien comentado es más fácil de seguir (especialmente cuando usted no fue quien lo escribió originalmente) que cuando se divide en múltiples funciones que nunca se usan en otro lugar. Pero realmente depende de la situación.

Creo que la conclusión principal es que cuando divide un bloque de código, está intercambiando un tipo de complejidad por otro. Probablemente hay un punto dulce en algún lugar en el medio.

Jonathan Tran
fuente
¿Has leído "Código limpio"? Discute esto extensamente. amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/…
Martin Wickman
9

Idealmente, toda la función debería ser visible sin tener que desplazarse. A veces, esto no es posible. Pero si puede dividirlo en pedazos, hará que leer el código sea mucho más fácil.

Sé que tan pronto como presione Page Up / Down o me mueva a una sección diferente del código, solo puedo recordar 7 +/- 2 cosas de la página anterior. Y desafortunadamente, algunas de esas ubicaciones se utilizarán al leer el nuevo código.

Siempre me gusta pensar en mi memoria a corto plazo como los registros de una computadora (CISC, no RISC). Si tiene la función completa en la misma página, puede ir al caché para obtener la información requerida de otra sección del programa. Si la función completa no cabe en una página, eso sería el equivalente de siempre empujar cualquier memoria al disco después de cada operación.

jsternberg
fuente
3
Solo necesita imprimirlo en una impresora matricial - vea, 10 metros de código de una vez :)
O obtenga un monitor con una resolución más alta
frogstarr78
Y úsalo en orientación vertical.
Calmarius
4

¿Por qué usar funciones anidadas, en lugar de funciones externas normales?

Incluso si las funciones externas solo se usan alguna vez en una función que antes era grande, todavía hace que todo el desastre sea más fácil de leer:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}
Fishtoaster
fuente
2
Dos posibles razones: desordenar el espacio de nombres y lo que llamaré "proximidad léxica". Estas pueden ser buenas o malas razones, dependiendo de su idioma.
Frank Shearar
Pero las funciones anidadas pueden ser menos eficientes porque necesitan soportar funargs hacia abajo y posiblemente cierres léxicos completos. Si el lenguaje está poco optimizado, puede hacer un trabajo adicional para crear un entorno dinámico para pasar a las funciones secundarias.
Kaz
3

No tengo el libro frente a mí en este momento (para citar), pero según Code Complete, el "punto dulce" para la longitud de la función era de alrededor de 25-50 líneas de código según su investigación.

Hay momentos en los que está bien tener funciones largas:

  • Cuando la complejidad ciclomática de la función es baja. Sus colegas desarrolladores pueden sentirse un poco frustrados si tienen que mirar una función que contiene una declaración if gigante y la declaración else para ese if no está en la pantalla al mismo tiempo.

Los momentos en que no está bien tener funciones largas:

  • Tiene una función con condicionales profundamente anidados. Haga un favor a sus compañeros lectores de código, mejore la legibilidad al romper la función. Una función le indica al lector que "este es un bloque de código que hace una cosa". También pregúntese si la longitud de la función indica que está haciendo demasiado y necesita ser factorizada a otra clase.

La conclusión es que la capacidad de mantenimiento debe ser una de las principales prioridades de su lista. Si otro desarrollador no puede ver su código y obtener una "idea" de lo que está haciendo el código en menos de 5 segundos, su código no proporciona suficientes "metadatos" para decir lo que está haciendo. Otros desarrolladores deberían poder decir qué está haciendo su clase simplemente mirando el navegador de objetos en su IDE elegido en lugar de leer más de 100 líneas de código.

Las funciones más pequeñas tienen las siguientes ventajas:

  • Portabilidad: es mucho más fácil mover la funcionalidad (ya sea dentro de la clase al refactorizar a otra diferente)
  • Depuración: cuando observa el stacktrace, es mucho más rápido detectar un error si está buscando una función con 25 líneas de código en lugar de 100.
  • Legibilidad: el nombre de la función indica qué está haciendo un bloque de código completo. Es posible que un desarrollador de tu equipo no quiera leer ese bloque si no está trabajando con él. Además, en la mayoría de los IDE modernos, otro desarrollador puede comprender mejor lo que hace su clase leyendo los nombres de las funciones en un navegador de objetos.
  • Navegación: la mayoría de los IDE le permitirán buscar el nombre de las funciones. Además, la mayoría de los IDE modernos tienen la capacidad de ver el origen de una función en otra ventana, esto les da a otros desarrolladores que pueden ver su función larga en 2 pantallas (si los monitores múltiples) en lugar de hacer que se desplace.

La lista continua.....

Korbin
fuente
2

La respuesta es que depende, sin embargo, probablemente debería convertir eso en una clase.

Lie Ryan
fuente
A menos que no esté escribiendo en un OOPL, en cuyo caso tal vez no :)
Frank Shearar
dsimcha mencionó que estaban usando D y Python.
frogstarr78
Las clases son a menudo muletas para las personas que nunca han oído hablar de cosas como cierres léxicos.
Kaz
2

No me gustan la mayoría de las funciones anidadas. Las lambdas caen en esa categoría, pero generalmente no me señalan a menos que tengan más de 30-40 caracteres.

La razón básica es que se convierte en una función localmente densa con recursividad semántica interna, lo que significa que es difícil para mí asimilar mi cerebro y es más fácil llevar algunas cosas a una función auxiliar que no satura el espacio de código .

Considero que una función debe hacer lo suyo. Hacer otras cosas es lo que hacen otras funciones. Entonces, si tiene una función de 200 líneas Doing Its Thing, y todo fluye, eso está bien.

Paul Nathan
fuente
A veces tiene que pasar tanto contexto a una función externa (a la que podría tener acceso conveniente como una función local) que hay más complicación en cómo se invoca la función que lo que hace.
Kaz
1

Es aceptable? Esa es realmente una pregunta que solo tú puedes responder. ¿La función logra lo que necesita? ¿Es mantenible? ¿Es 'aceptable' para los otros miembros de su equipo? Si es así, eso es lo que realmente importa.

Editar: no vi lo que pasa con las funciones anidadas. Personalmente, no los usaría. En su lugar, usaría funciones regulares.

Gran maestro B
fuente
1

Una función larga "en línea recta" puede ser una forma clara de especificar una secuencia larga de pasos que siempre ocurren en una secuencia particular.

Sin embargo, como otros han mencionado, esta forma es propensa a tener tramos locales de complejidad en los que el flujo general es menos evidente. Colocar esa complejidad local en una función anidada (es decir, definida en otro lugar dentro de la función larga, quizás en la parte superior o inferior) puede restaurar la claridad del flujo de la línea principal.

Una segunda consideración importante es controlar el alcance de las variables que están destinadas a ser utilizadas solo en un tramo local de una función larga. Se requiere vigilancia para evitar tener una variable que se introdujo en una sección del código referenciada involuntariamente en otro lugar (por ejemplo, después de ciclos de edición), ya que este tipo de error no aparecerá como un error de compilación o tiempo de ejecución.

En algunos idiomas, este problema se puede evitar fácilmente: un tramo de código local se puede envolver en su propio bloque, como con "{...}", dentro del cual las variables recién introducidas solo son visibles para ese bloque. Algunos lenguajes, como Python, carecen de esta característica, en cuyo caso las funciones locales pueden ser útiles para imponer regiones de menor alcance.

gwideman
fuente
1

No, las funciones de varias páginas no son deseables y no deben pasar la revisión del código. Solía ​​escribir funciones largas también, pero después de leer Refactorización de Martin Fowler , me detuve. Las funciones largas son difíciles de escribir correctamente, difíciles de entender y de probar. Nunca he visto una función de incluso 50 líneas que no se entendería y probaría más fácilmente si se dividiera en un conjunto de funciones más pequeñas. En una función de varias páginas, es casi seguro que haya clases enteras que se deben factorizar. Es difícil ser más específico. Tal vez debería publicar una de sus funciones largas en Code Review y alguien (tal vez yo) puede mostrarle cómo mejorarla.

Kevin Cline
fuente
0

Cuando estoy programando en Python, me gusta dar un paso atrás después de haber escrito una función y preguntarme si se adhiere al "Zen de Python" (escriba 'importar esto' en su intérprete de Python):

Hermoso es mejor que feo.
Explícito es mejor que implícito.
Simple es mejor que complejo.
Complejo es mejor que complicado.
Plano es mejor que anidado.
Escaso es mejor que denso.
La legibilidad cuenta.
Los casos especiales no son lo suficientemente especiales como para romper las reglas.
Aunque la practicidad supera la pureza.
Los errores nunca deben pasar en silencio.
A menos que sea silenciado explícitamente.
Ante la ambigüedad, rechaza la tentación de adivinar.
Debe haber una, y preferiblemente solo una, forma obvia de hacerlo.
Aunque esa manera puede no ser obvia al principio a menos que seas holandés.
Ahora es mejor que nunca.
Aunque nunca es mejor que lo correctoahora.
Si la implementación es difícil de explicar, es una mala idea.
Si la implementación es fácil de explicar, puede ser una buena idea.
Los espacios de nombres son una gran idea, ¡hagamos más de eso!


fuente
1
Si explícito es mejor que implícito, debemos codificar en lenguaje máquina. Ni siquiera en ensamblador; hace cosas implícitas desagradables como llenar automáticamente los espacios de retardo de rama con instrucciones, calcular las compensaciones de rama relativas y resolver referencias simbólicas entre globales. Hombre, estos tontos usuarios de Python y su pequeña religión ...
Kaz
0

Póngalos en un módulo separado.

Asumiendo que su solución no está más hinchada que la necesidad, no tiene demasiadas opciones. Ya ha dividido las funciones en diferentes subfunciones, por lo que la pregunta es dónde debe colocarlo:

  1. Póngalos en un módulo con una sola función "pública".
  2. Póngalos en una clase con una sola función "pública" (estática).
  3. Anidéalos en una función (como has descrito).

Ahora, tanto la segunda como la tercera alternativa anidan en cierto sentido, pero la segunda alternativa para algunos programadores no parece estar mal. Si no descarta la segunda alternativa, no veo demasiadas razones para descartar la tercera.

Rey del cielo
fuente