¿Debo extraer una funcionalidad específica en una función y por qué?

29

Tengo un método grande que realiza 3 tareas, cada una de ellas se puede extraer en una función separada. Si realizaré funciones adicionales para cada una de esas tareas, ¿mejorará o empeorará mi código y por qué?

Obviamente, tendrá menos líneas de código en la función principal, pero habrá declaraciones de funciones adicionales, por lo que mi clase tendrá métodos adicionales, lo que creo que no es bueno, porque hará que la clase sea más compleja.

¿Debo hacer eso antes de escribir todo el código o debo dejarlo hasta que todo esté hecho y luego extraer las funciones?

dhblah
fuente
19
"Lo dejo hasta que todo esté hecho" suele ser sinónimo de "Nunca se hará".
Eufórico el
2
Eso es generalmente cierto, pero también recuerda el principio opuesto de YAGNI (que no se aplica en este caso, ya que ya lo necesitas).
jhocking
Solo quería enfatizar que no se centre tanto en reducir las líneas de código. En cambio, trate de pensar en términos de abstracciones. Cada función debe tener solo un trabajo. Si encuentra que sus funciones están haciendo más de un trabajo, entonces generalmente debe refactorizar el método. Si sigue estas pautas, debería ser casi imposible tener funciones demasiado largas.
Adrian

Respuestas:

35

Este es un libro que a menudo enlazo, pero aquí voy de nuevo: el Código Limpio de Robert C. Martin , capítulo 3, "Funciones".

Obviamente, tendrá menos líneas de código en la función principal, pero habrá declaraciones de funciones adicionales, por lo que mi clase tendrá métodos adicionales, lo que creo que no es bueno, porque hará que la clase sea más compleja.

¿Prefiere leer una función con +150 líneas, o una función que llame a 3 +50 líneas? Creo que prefiero la segunda opción.

, mejorará su código en el sentido de que será más "legible". Haga funciones que realicen una y solo una cosa, serán más fáciles de mantener y producir un caso de prueba.

Además, una cosa muy importante que aprendí con el libro antes mencionado: elija nombres buenos y precisos para sus funciones. Cuanto más importante es la función, más preciso debe ser el nombre. No se preocupe por la longitud del nombre, si debe llamarse FunctionThatDoesThisOneParticularThingOnly, asígnele el nombre de esa manera.

Antes de realizar su refactorización, escriba uno o más casos de prueba. Asegúrate de que funcionen. Una vez que haya terminado con su refactorización, podrá iniciar estos casos de prueba para asegurarse de que el nuevo código funcione correctamente. Puede escribir pruebas "más pequeñas" adicionales para garantizar que sus nuevas funciones funcionen bien separadamente.

Finalmente, y esto no es contrario a lo que acabo de escribir, pregúntese si realmente necesita hacer esta refactorización, consulte las respuestas a "¿ Cuándo refactorizar ?" (también, busque SO preguntas sobre "refactorización", hay más y las respuestas son interesantes para leer)

¿Debo hacer eso antes de escribir todo el código o debo dejarlo hasta que todo esté hecho y luego extraer las funciones?

Si el código ya está allí y funciona y tiene poco tiempo para la próxima versión, no lo toque. De lo contrario, creo que uno debería hacer pequeñas funciones siempre que sea posible y, como tal, refactorizar cada vez que haya tiempo disponible y asegurarse de que todo funcione como antes (casos de prueba).

Jalayn
fuente
10
En realidad, Bob Martin ha demostrado varias veces que prefiere 7 funciones con 2 a 3 líneas sobre una función con 15 líneas (consulte aquí sites.google.com/site/unclebobconsultingllc/… ). Y ahí es donde muchos desarrolladores experimentados se resistirán. Personalmente, creo que muchos de esos "desarrolladores experimentados" simplemente tienen problemas para aceptar que aún podrían mejorar algo tan básico como construir abstracciones con funciones después de> 10 años de codificación.
Doc Brown
+1 solo por hacer referencia a un libro que, para mi modesta opinión, debería estar en los estantes de cualquier compañía de software.
Fabio Marcolini
3
Podría estar parafraseando aquí, pero una frase de ese libro que resuena en mi cabeza casi todos los días es "cada función debe hacer una sola cosa y hacerlo bien". Parece particularmente relevante aquí ya que el OP dijo "mi función principal hace tres cosas"
wakjah
¡Estás absolutamente en lo correcto!
Jalayn
Depende de cuánto estén entrelazadas las tres funciones separadas. Puede ser más fácil seguir un bloque de código que se encuentra en un solo lugar que tres bloques de código que se basan repetidamente entre sí.
user253751
13

Si obviamente. Si es fácil ver y separar las diferentes "tareas" de una sola función.

  1. Legibilidad: las funciones con buenos nombres hacen explícito lo que hace el código sin necesidad de leer ese código.
  2. Reutilización: es más fácil usar una función que hace una cosa en múltiples lugares, que tener una función que hace cosas que no necesita.
  3. Capacidad de prueba: es más fácil probar la función, que tiene una "función" definida, aquella que tiene muchas de ellas

Pero puede haber problemas con esto:

  • No es fácil ver cómo separar la función. Esto puede requerir la refactorización del interior de la función primero, antes de pasar a la separación.
  • La función tiene un gran estado interno, que se pasa. Esto generalmente requiere algún tipo de solución OOP.
  • Es difícil saber qué función debería estar haciendo. Unidad de prueba y refactorizar hasta que sepa.
Eufórico
fuente
5

El problema que plantea no es un problema de codificación, convenciones o práctica de codificación, sino un problema de legibilidad y formas en que los editores de texto muestran el código que escribe. Este mismo problema aparece también en la publicación:

¿Está bien dividir las funciones y los métodos largos en otros más pequeños a pesar de que nada más los llamará?

Dividir una función en subfunciones tiene sentido al implementar un gran sistema con la intención de encapsular las diferentes funcionalidades de las que estará compuesto. Sin embargo, tarde o temprano, te encontrarás con una serie de grandes funciones. Algunos de ellos son ilegibles y difíciles de mantener, ya sea que los mantenga como funciones largas individuales o las divida en funciones más pequeñas. Esto es particularmente cierto para las funciones donde las operaciones que realiza no son necesarias en ningún otro lugar de su sistema. Permite recoger una de estas funciones tan largas y considerarla en una vista más amplia.

Pro:

  • Una vez que lo lea, tiene una idea completa de todas las opciones que realiza la función (puede leerlo como un libro);
  • Si desea depurarlo, puede ejecutarlo paso a paso sin saltar a ningún otro archivo / parte del archivo;
  • Tiene la libertad de acceder / usar cualquier variable declarada en cualquier etapa de la función;
  • El algoritmo que implementa la función está completamente contenido en la función (encapsulado);

Contra:

  • Toma muchas páginas de tu pantalla;
  • Toma mucho tiempo leerlo;
  • No es fácil memorizar todos los diferentes pasos;

Ahora imaginemos dividir la función larga en varias subfunciones y analizarlas con una perspectiva más amplia.

Pro:

  • Excepto las funciones de licencia, cada función describe con palabras (nombres de subfunciones) los diferentes pasos realizados;
  • Lleva muy poco tiempo leer cada función / subfunción;
  • Está claro qué parámetros y variables se ven afectados en cada subfunción (separación de preocupaciones);

Contra:

  • Es fácil imaginar lo que hace una función como "sin ()", pero no es tan fácil imaginar lo que hacen nuestras subfunciones;
  • El algoritmo ahora está desaparecido, ahora se distribuye en subfunciones de mayo (sin descripción general);
  • Al depurarlo paso a paso, es fácil olvidar la llamada de función de nivel de profundidad de la que proviene (saltando aquí y allá en los archivos de su proyecto);
  • Puede perder contexto fácilmente al leer las diferentes subfunciones;

Ambas soluciones tienen pro y contra. La mejor solución real sería tener editores que permitan expandir, en línea y en toda su profundidad, cada función llamada en su contenido. Lo que haría que la división de funciones en subfunciones sea la mejor solución.

Antonello Ceravola
fuente
2

Para mí hay cuatro razones para extraer bloques de código en funciones:

  • Lo está reutilizando : acaba de copiar un bloque de código en el portapapeles. En lugar de pegarlo, póngalo en una función y reemplace el bloque con una llamada a función en ambos lados. Entonces, cada vez que necesite cambiar ese bloque de código, solo necesita cambiar esa única función en lugar de cambiar el código en varios lugares. Entonces, cada vez que copie un bloque de código, debe hacer una función.

  • Es una devolución de llamada : es un controlador de eventos o algún tipo de código de usuario que llama una biblioteca o un marco. (Apenas puedo imaginar esto sin hacer funciones).

  • Cree que se reutilizará , en el proyecto actual o tal vez en otro lugar: acaba de escribir un bloque que calcula la subsecuencia común más larga de dos matrices. Incluso si su programa llama a esta función solo una vez, creo que necesitaré esta función eventualmente en otros proyectos también.

  • Desea un código autodocumentado : por lo tanto, en lugar de escribir una línea de comentario sobre un bloque de código que resuma lo que hace, extrae todo en una función y lo nombra como lo que escribiría en un comentario. Aunque no soy fanático de esto, porque me gusta escribir el nombre del algoritmo utilizado, la razón por la que elegí ese algoritmo, etc. Los nombres de las funciones serían demasiado largos entonces ...

Calmarius
fuente
1

Estoy seguro de que ha escuchado el consejo de que las variables deben tener un alcance lo más estricto posible, y espero que esté de acuerdo con eso. Bueno, las funciones son contenedores de alcance, y en funciones más pequeñas el alcance de las variables locales es más pequeño. Es mucho más claro cómo y cuándo se supone que deben usarse y es más difícil usarlos en el orden incorrecto o antes de que se inicialicen.

Además, las funciones son contenedores de flujo lógico. Solo hay una entrada, las salidas están claramente marcadas, y si la función es lo suficientemente corta, los flujos internos deberían ser obvios. Esto tiene el efecto de reducir la complejidad ciclomática, que es una forma confiable de reducir la tasa de defectos.

John Wu
fuente
0

Aparte: escribí esto en respuesta a la pregunta de Dallin (ahora cerrada) pero todavía siento que podría ser útil para alguien, así que aquí va


Creo que la razón para atomizar las funciones es 2 veces mayor, y como menciona @jozefg, depende del lenguaje utilizado.

Separación de intereses

La razón principal para hacer esto es mantener diferentes partes de código separadas, por lo que cualquier bloque de código que no contribuya directamente al resultado / intento deseado de la función es una preocupación separada y podría extraerse.

Supongamos que tiene una tarea en segundo plano que también actualiza una barra de progreso, la actualización de la barra de progreso no está directamente relacionada con la tarea de ejecución prolongada, por lo que debe extraerse, incluso si es el único fragmento de código que usa la barra de progreso.

Digamos que en JavaScript tiene una función getMyData (), que 1) crea un mensaje de jabón a partir de parámetros, 2) inicializa una referencia de servicio, 3) llama al servicio con el mensaje de jabón, 4) analiza el resultado, 5) devuelve el resultado. Parece razonable, he escrito esta función exacta muchas veces, pero en realidad eso podría dividirse en 3 funciones privadas que solo incluyen el código para 3 y 5 (si es así) ya que ninguno de los otros códigos es directamente responsable de obtener datos del servicio .

Experiencia de depuración mejorada

Si tiene funciones completamente atómicas, su seguimiento de pila se convierte en una lista de tareas, que enumera todo el código ejecutado con éxito, es decir:

  • Obtén mis datos
    • Crear mensaje de jabón
    • Inicializar referencia de servicio
    • Respuesta de servicio analizado - ERROR

sería mucho más interesante que descubrir que hubo un error al obtener los datos. Pero algunas herramientas son aún más útiles para depurar árboles de llamadas detallados que, por ejemplo, tomar Microsofts Debugger Canvas .

También entiendo su preocupación de que puede ser difícil seguir el código escrito de esta manera porque al final del día, debe elegir un orden de funciones en un solo archivo donde su árbol de llamadas sería mucho más complejo que eso. . Pero si las funciones se nombran bien (intellisense me permite usar 3-4 palabras mayúsculas y minúsculas en cualquier función, por favor, sin retrasarme) y estructurado con una interfaz pública en la parte superior del archivo, su código se leerá como un pseudocódigo que es, con mucho, la forma más fácil de obtener una comprensión de alto nivel de una base de código.

FYI: esta es una de esas cosas de "haz lo que digo, no lo que hago", mantener el código atómico no tiene sentido a menos que seas implacablemente coherente con eso, en mi humilde opinión, lo que no soy.

Dead.Rabit
fuente