¿Está bien dividir las funciones y los métodos largos en otros más pequeños a pesar de que ninguna otra cosa los llamará? [duplicar]

162

Últimamente he estado tratando de dividir los métodos largos en varios métodos cortos.

Por ejemplo: tengo una función process_url() que divide las URL en componentes y luego las asigna a algunos objetos a través de sus métodos. En lugar de implementar todo esto en una función, solo preparo la URL para dividir en process_url() , y luego la paso a la función process_components() , que luego pasa los componentes a la función assign_components() .

Al principio, esto parecía mejorar la legibilidad, porque en lugar de los enormes métodos y funciones de "Dios" tenía otros más pequeños con nombres más descriptivos. Sin embargo, al examinar un código que he escrito de esa manera, he descubierto que ahora no tengo idea de si otras funciones o métodos llaman a estas funciones más pequeñas.

Ejemplo anterior continuo: alguien que mira el código podría pensar que la funcionalidad process_components() se abstrae en una función porque se llama mediante varios métodos y funciones, cuando en realidad solo es llamada por process_url() .

Esto parece un poco mal. La alternativa es seguir escribiendo métodos y funciones largos, pero indicar sus secciones con comentarios.

¿Es incorrecta la técnica de división de funciones que describí? ¿Cuál es la forma preferida de administrar grandes funciones y métodos?

ACTUALIZACIÓN: Mi principal preocupación es que abstraer el código en una función podría implicar que podría ser llamado por muchas otras funciones.

VÉASE TAMBIÉN: las discusiones sobre reddit en / r / schedule (proporciona una perspectiva diferente en lugar de la mayoría de las respuestas aquí) y / r / readablecode .

    
pregunta sbichenko 24.04.2013 - 18:31
fuente

13 respuestas

225

Probar el código que hace muchas cosas es difícil.

El código de depuración que hace muchas cosas es difícil.

La solución a estos dos problemas es escribir código que no haga muchas cosas. Escribe cada función para que haga una cosa y solo una cosa. Esto los hace fáciles de probar con una prueba de unidad (uno no necesita docenas de docenas de pruebas de unidad).

Un compañero de trabajo mío tiene la frase que usa cuando juzga si un método determinado debe dividirse en otros más pequeños:

  

Si, al describir la actividad del código a otro programador, usa la palabra 'y', el método debe dividirse en al menos una parte más.

Usted escribió:

  

Tengo una función process_url () que divide las URL en componentes y luego las asigna a algunos objetos a través de sus métodos.

Esto debería ser al menos dos métodos. Está bien envolverlos en un método público, pero el funcionamiento debe ser dos métodos diferentes.

    
respondido por el user40980 24.04.2013 - 18:41
fuente
77

Sí, dividir funciones largas es normal. Esta es una forma de hacer las cosas que alienta Robert C. Martin en su libro Clean Code . En particular, debe elegir nombres muy descriptivos para sus funciones, como una forma de código de auto-documentación.

    
respondido por el Scott Whitlock 24.04.2013 - 18:34
fuente
39

Como señalaron las personas, esto mejora la legibilidad. Una persona que lee process_url() puede ver más claramente cuál es el proceso general para tratar las URL solo con leer algunos nombres de métodos.

El problema es que otras personas pueden pensar que esas funciones son utilizadas por alguna otra parte del código, y si es necesario cambiar algunas de ellas, pueden optar por conservar esas funciones y definir otras nuevas. Esto significa que algún código se vuelve inalcanzable.

Hay varias maneras de lidiar con esto. Primero está la documentación y los comentarios en el código. En segundo lugar, herramientas que proporcionan pruebas de cobertura. En cualquier caso, en gran medida esto depende del lenguaje de programación, estas son algunas de las soluciones que puede aplicar según el lenguaje de programación:

  • los lenguajes orientados a objetos pueden permitir definir algunos métodos privados, para garantizar que no se utilicen en ningún otro lado
  • los módulos en otros idiomas pueden especificar qué funciones son visibles desde el exterior, etc.
  • los lenguajes de muy alto nivel como Python pueden eliminar la necesidad de definir varias funciones porque, de todos modos, serían simples lineas simples
  • otros idiomas como Prolog pueden requerir (o sugerir fuertemente) la definición de un nuevo predicado para cada salto condicional.
  • en algunos casos es común definir funciones auxiliares dentro de la función que las usa (funciones locales), algunas veces son funciones anónimas (cierres de código), esto es común en las funciones de devolución de llamada de Javascript.

En resumen, dividir en varias funciones suele ser una buena idea en términos de legibilidad. Puede que no sea realmente bueno si las funciones son muy cortas y esto crea el efecto goto o si los nombres no son realmente descriptivos, en ese caso, leer un código requeriría saltar entre las funciones, lo que puede ser complicado. Sobre su preocupación sobre el alcance y el uso de estas funciones, hay varias formas de tratarlas que dependen del idioma en general.

En general, el mejor consejo es usar el sentido común. Cualquier regla estricta es muy probable que sea incorrecta en algún caso y al final depende de la persona. Yo consideraría esto legible:

process_url = lambda url: dict(re.findall('([^?=&]*)=([^?=&]*)', url))

Personalmente, prefiero un forro, aunque sea ligeramente complejo en lugar de saltar y buscar en varios archivos de código, si me demoran más de tres segundos en encontrar otra parte del código que pueda Ni siquiera sabía qué estaba revisando de todos modos. Las personas que no padecen TDAH pueden preferir más nombres explicativos que puedan recordar, pero al final lo que siempre está haciendo es equilibrar la complejidad en los diferentes niveles de código, líneas, párrafos, funciones, archivos, módulos, etc.

Por lo tanto, la palabra clave es balance . Una función con mil líneas es un infierno para cualquiera que la lea, porque no hay encapsulación y el contexto se vuelve demasiado grande. Una función dividida en mil funciones, cada una de ellas con una línea puede ser peor:

  • tiene algunos nombres (que podría haber proporcionado como comentarios en las líneas)
  • está (con suerte) eliminando variables globales y no tiene que preocuparse por el estado (con transparencia referencial)
  • pero obligas a los lectores a saltar de un lado a otro.

Aquí no hay balas de plata, sino experiencia y equilibrio. En mi humilde opinión, la mejor manera de aprender cómo hacerlo es leer un montón de código escrito por otras personas y analizar por qué es difícil para usted leerlo y cómo hacerlo más legible. Eso proporcionaría una valiosa experiencia.

    
respondido por el Trylks 24.04.2013 - 19:26
fuente
17

Yo diría que depende.

Si solo lo está dividiendo por el simple hecho de dividir y llamarlos nombres como process_url_partN y así sucesivamente, entonces NO , no lo haga. Simplemente hace que sea más difícil de seguir más adelante cuando usted o alguien más necesita averiguar qué está pasando.

Si está sacando métodos con propósitos claros que pueden ser probados por ellos mismos y tienen sentido por sí mismos (incluso si nadie más los está usando), entonces SÍ .

Para su propósito particular, parece que tiene dos objetivos.

  1. Analice una URL y devuelva una lista de sus componentes.
  2. Haz algo con esos componentes.

Escribiría la primera parte por separado y obtendría un resultado bastante general que podría probarse fácilmente y posiblemente reutilizarse más tarde. Aún mejor, buscaría una función incorporada que ya lo haga en su lenguaje / marco y la use, a menos que su análisis sea muy especial. Si es súper especial, todavía lo escribiría como un método separado, pero probablemente lo "agruparía" como un método privado / protegido en la clase que administra la segunda (si está escribiendo código orientado a objetos).

La segunda parte escribiría como su propio componente que usa la primera para el análisis de URL.

    
respondido por el Svish 25.04.2013 - 13:40
fuente
14

Nunca he tenido problemas con otros desarrolladores que dividen métodos más grandes en métodos más pequeños, ya que es un patrón que yo sigo. El método de "Dios" es una trampa terrible en la que caer, y otros que tienen menos experiencia o simplemente no les importa, tienden a ser atrapados más a menudo que no. Dicho esto ...

Es increíblemente importante usar los identificadores de acceso apropiados en los métodos más pequeños. Es frustrante encontrar una clase llena de pequeños métodos públicos porque entonces pierdo totalmente la confianza en encontrar dónde y cómo se usa el método en toda la aplicación.

Vivo en C # -land, así que tenemos public , private , protected , internal , y ver esas palabras me muestra más allá de toda duda el alcance del método y dónde debo mirar para llamadas. Si es privado, sé que el método se usa en una sola clase y tengo plena confianza al refactorizar.

En el mundo de Visual Studio, tener múltiples soluciones ( .sln ) exacerba este antipatrón porque los ayudantes de "Encontrar Usos" de IDE / Resharper no encontrarán usos fuera de la solución abierta.

    
respondido por el Ryan Rodemoyer 25.04.2013 - 21:37
fuente
11

Si su lenguaje de programación lo admite, es posible que pueda definir sus funciones "auxiliares" dentro del alcance de su función process_url() para obtener los beneficios de legibilidad de funciones separadas. por ejemplo

function process_url(url) {

    function foo(a) {
        // ...
    }

    function bar(a) {
        // ...
    }

    return [ foo(url), bar(url) ];

}

Si su lenguaje de programación no admite esto, puede mover foo() y bar() fuera del alcance de process_url() (para que sea visible a otras funciones / métodos), pero considere esto como " hackear "que has implementado porque tu lenguaje de programación no es compatible con esta función.

Si dividir una función en subfunciones probablemente dependerá de si hay nombres significativos / útiles para las partes y de qué tan grande sea cada una de las funciones, entre otras consideraciones.

    
respondido por el mjs 25.04.2013 - 16:28
fuente
9

Si alguien está interesado en alguna literatura sobre esta pregunta: esto es exactamente a lo que Joshua Kerievsky se refiere como "Método de composición" en su "Refactorización de patrones" (Addison-Wesley):

  

Transforme la lógica en un pequeño número de pasos que revelan la intención al mismo nivel de detalle.

Creo que la anidación correcta de los métodos de acuerdo con su "nivel de detalle" es importante aquí.

Consulte un extracto en el sitio del editor:

  

Gran parte del código que escribimos no comienza siendo simple. Para simplificarlo, debemos reflexionar sobre lo que no es simple al respecto y preguntarnos continuamente: "¿Cómo podría ser más simple?" A menudo podemos simplificar el código considerando una solución completamente diferente. Las refactorizaciones en este capítulo presentan diferentes soluciones para simplificar los métodos, las transiciones de estado y las estructuras de árbol.

     

Compose Method (123) se trata de producir métodos que comuniquen de manera eficiente lo que hacen y cómo hacen lo que hacen. Un método compuesto [Beck, SBPP] consiste en llamadas a métodos bien nombrados que están todos en el mismo nivel de detalle. Si desea mantener su sistema simple, intente aplicar Compose Method (123) en todas partes ...

     

Addendum:KentBeck( "Patrones de implementación" ) se refiere a como "Método Compuesto". Te aconseja que:

  

[c] elabora métodos a partir de llamadas a otros métodos, cada uno de los cuales se encuentra aproximadamente en la   Mismo nivel de abstracción.   Uno de los signos de un método mal compuesto es una mezcla de abstracción   niveles [.]

Ahí está, nuevamente, la advertencia de no mezclar diferentes niveles de abstracción (énfasis mío).

    
respondido por el Sebastian 01.05.2013 - 09:47
fuente
5

Una buena regla es tener abstracciones cercanas en niveles similares (mejor formulado por sebastian en esto responder justo arriba.)

I.e. Si tiene una función (grande) que se ocupa de cosas de bajo nivel, pero también hace algunas elecciones de nivel superior, intente factorizar las cosas de bajo nivel:

void foo() {

     if(x) {
       y = doXformanysmallthings();
     }

     z = doYforthings(y);

     if (z != y && isFullmoon()) {
         launchSpacerocket()
     }
}

Mover cosas a funciones más pequeñas suele ser mejor que tener muchos bucles y cosas así dentro de una función que consiste en unos pocos pasos "grandes" conceptuales. (A menos que pueda combinar eso en expresiones LINQ / foreach / lambda relativamente pequeñas ...)

    
respondido por el Macke 26.04.2013 - 16:31
fuente
4

Si pudiera diseñar una clase que sea apropiada para estas funciones, hágalas privadas. Dicho de otra manera, con una definición de clase adecuada puede exponer lo único que necesita exponer.

    
respondido por el Tom Haley 24.04.2013 - 19:59
fuente
4

Estoy seguro de que esta no será la opinión popular, pero está perfectamente bien. La localidad de Referencia puede ser una gran ayuda para asegurarse de que usted y otros entiendan la función (en este caso me refiero al código y no a la memoria específicamente).

Como con todo, es un equilibrio. Debería preocuparse más por cualquiera que le diga 'siempre' o 'nunca'.

    
respondido por el Fred 25.04.2013 - 15:33
fuente
3

Considere esta función simple (estoy usando una sintaxis similar a Scala pero espero que la idea sea clara sin ningún conocimiento de Scala):

def myFun ... {
    ...
    if (condition1) {
        ...
    } else {
        ...
    }
    if (condition2) {
        ...
    } else {
        ...
    }
    if (condition3) {
        ...
    } else {
        ...
    }
    // rest
    ...
}

Esta función tiene hasta 8 rutas posibles de cómo se puede ejecutar su código, dependiendo de cómo se evalúen esas condiciones.

  • Esto significa que necesitarás 8 pruebas diferentes solo para probar esta parte. Además, lo más probable es que algunas combinaciones no sean posibles, y luego tendrá que analizar cuidadosamente cuáles son (y asegúrese de no perder algunas de las posibles), mucho trabajo.
  • Es muy difícil razonar sobre el código y su corrección. Como cada uno de los bloques if y su condición pueden depender de algunas variables locales compartidas, para saber qué sucede después de ellos, todos los que trabajan con el código deben analizar todos esos bloques de código y las 8 rutas de ejecución posibles. Es muy fácil cometer un error aquí y lo más probable es que alguien que actualice el código se pierda algo y presente un error.

Por otro lado, si estructura el cálculo como

def myFun ... {
    ...
    val result1 = myHelper1(...);
    val result2 = myHelper2(...);
    val result3 = myHelper3(...);
    // rest
    ...
}

private def myHelper1(/* arguments */): SomeResult = {
    if (condition1) {
        ...
    } else {
        ...
    }
}
// Similarly myHelper2 and myHelper3

puedes:

  • Pruebe fácilmente cada una de las funciones auxiliares, cada una de ellas tiene solo dos vías posibles de ejecución.
  • Al examinar myFun es inmediatamente obvio si result2 depende de result1 (solo verificando si la llamada a myHelper2(...) lo usa para calcular uno de los argumentos. (Suponiendo que los ayudantes no usen algunos estado global). También es obvio que cómo son dependientes, algo que es mucho más difícil de entender en el caso anterior. Además, después de las tres llamadas, también queda claro cómo se ve el estado del cálculo. - se captura solo en result1 , result2 y result3 - no es necesario verificar si / qué otras variables locales se han modificado.
respondido por el Petr Pudlák 26.04.2013 - 09:23
fuente
2

Cuanto más concreta sea la responsabilidad de un método, más fácil de probar, leer y mantener será el código. Aunque ningún otro los llama.

Si en el futuro necesita usar esta funcionalidad de otros lugares, puede extraer esos métodos fácilmente.

    
respondido por el lucasvc 25.04.2013 - 08:42
fuente
-5

Es perfectamente aceptable nombrar tus métodos de esta forma:

MyProcedure()
  MyProcedure_part1()
  MyProcedure_part2()
end;

No veo por qué alguien pensaría que estos serían llamados por algún otro proceso, y está perfectamente claro para qué sirven.

    
respondido por el Colin Nicholls 25.04.2013 - 19:25
fuente

Lea otras preguntas en las etiquetas