¿Los métodos privados con un solo estilo de referencia son incorrectos?

139

Generalmente, utilizo métodos privados para encapsular la funcionalidad que se reutiliza en varios lugares de la clase. Pero a veces tengo un gran método público que podría dividirse en pasos más pequeños, cada uno en su propio método privado. Esto haría que el método público sea más corto, pero me preocupa que obligar a cualquiera que lea el método a saltar a diferentes métodos privados dañe la legibilidad.

¿Hay consenso sobre esto? ¿Es mejor tener métodos públicos largos, o dividirlos en pedazos más pequeños incluso si cada uno no es reutilizable?

    
pregunta Jordak 05.06.2017 - 16:46

6 respuestas

202

No, este no es un mal estilo. De hecho es un muy buen estilo.

Las funciones privadas no necesitan existir simplemente debido a la reutilización. Esa es sin duda una buena razón para crearlos, pero hay otra: la descomposición.

Considera una función que hace demasiado. Tiene cien líneas de largo, y es imposible razonar sobre eso.

Si divide esta función en partes más pequeñas, todavía "hace" tanto trabajo como antes, pero en partes más pequeñas. Llama a otras funciones que deberían tener nombres descriptivos. La función principal se lee casi como un libro: haz A, luego B, luego C, etc. Las funciones a las que llama solo pueden llamarse en un lugar, pero ahora son más pequeñas. Cualquier función particular es necesariamente un espacio aislado de las otras funciones: tienen diferentes ámbitos.

Cuando descompones un problema grande en problemas más pequeños, incluso si esos problemas (funciones) más pequeños solo se usan / resuelven una vez, obtienes varios beneficios:

  • Legibilidad. Nadie puede leer una función monolítica y entender completamente lo que hace. Puedes seguir mintiéndote o dividirlo en trozos pequeños que tengan sentido.

  • Localidad de referencia. Ahora es imposible declarar y usar una variable, luego mantenerla fija y usarla de nuevo 100 líneas más tarde. Estas funciones tienen diferentes ámbitos.

  • Pruebas. Si bien solo es necesario realizar una prueba unitaria de los miembros públicos de una clase, puede ser conveniente probar también a ciertos miembros privados. Si hay una sección crítica de una función larga que podría beneficiarse de la prueba, es imposible probarlo de forma independiente sin extraerla a una función separada.

  • Modularidad. Ahora que tiene funciones privadas, puede encontrar una o más de ellas que podrían extraerse en una clase separada, ya sea que se use solo aquí o sea reutilizable. Hasta el punto anterior, es probable que esta clase separada también sea más fácil de probar, ya que necesitará una interfaz pública.

La idea de dividir el código grande en partes más pequeñas que sean más fáciles de entender y probar es un punto clave de El libro del tío Bob's Clean Código . En el momento de escribir esta respuesta, el libro tiene nueve años, pero es tan relevante hoy como lo era en aquel entonces.

    
respondido por el user22815 05.06.2017 - 17:04
36

¡Es probablemente una gran idea!

Tengo problemas al dividir largas secuencias de acción lineales en funciones separadas solo para reducir la longitud promedio de la función en tu base de código:

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

Ahora que ha agregado líneas de fuente y ha reducido considerablemente la legibilidad total. Especialmente si ahora está pasando muchos parámetros entre cada función para realizar un seguimiento del estado. ¡Ay!

Sin embargo , si ha logrado extraer una o más líneas en una función pura que cumple un único y claro propósito ( incluso si se llama solo una vez ), entonces has mejorado la legibilidad:

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

Es probable que esto no sea fácil en situaciones del mundo real, pero las piezas de funcionalidad pura a menudo se pueden descifrar si lo piensas lo suficiente.

Sabrás que estás en el camino correcto cuando tienes funciones con buenos nombres verbales y cuando la función principal las llama y todo se lee prácticamente como un párrafo de prosa.

Luego, cuando regrese semanas más tarde para agregar más funciones y descubra que realmente puede reutilizar una de esas funciones, entonces ¡oh, alegría! ¡Qué maravilloso y radiante placer!

    
respondido por el DaveGauer 05.06.2017 - 18:41
19

La respuesta es que depende mucho de la situación. @Snowman cubre los aspectos positivos de dividir una gran función pública, pero es importante recordar que también puede haber efectos negativos, como le preocupa.

  • Muchas funciones privadas con efectos secundarios pueden hacer que el código sea difícil de leer y bastante frágil. Esto es especialmente cierto cuando estas funciones privadas dependen de los efectos secundarios de cada una. Evite tener funciones estrechamente acopladas.

  • Las abstracciones tienen fugas . Si bien es bueno fingir cómo se realiza una operación o cómo se almacenan los datos, no importa, hay casos en los que lo hace, y es importante identificarlos.

  • La semántica y el contexto son importantes. Si no logra captar claramente lo que hace una función en su nombre, nuevamente puede reducir la legibilidad y aumentar la fragilidad de la función pública. Esto es especialmente cierto cuando comienza a crear funciones privadas con un gran número de parámetros de entrada y salida. Y mientras que desde la función pública, ves qué funciones privadas llama, desde la función privada, no ves cómo las funciones públicas lo llaman. Esto puede llevar a una "corrección de errores" en una función privada que rompe la función pública.

  • El código muy descompuesto siempre es más claro para el autor que para los demás. Esto no significa que no pueda ser claro para los demás, pero es fácil decir que tiene mucho sentido que se debe llamar a bar() antes de foo() en el momento de escribir.

  • Reutilización peligrosa. Su función pública probablemente restringió qué entrada fue posible para cada función privada. Si estas suposiciones de entrada no se capturan correctamente (documentar TODAS las suposiciones es difícil), alguien puede reutilizar indebidamente una de sus funciones privadas, introduciendo errores en la base de código.

Descomponga funciones en función de su cohesión interna y acoplamiento, no de longitud absoluta.

    
respondido por el dlasalle 05.06.2017 - 19:05
2

En mi humilde opinión, el valor de extraer bloques de código simplemente como un medio para romper la complejidad, a menudo estaría relacionado con la diferencia en la complejidad entre:

  1. Una descripción completa y precisa en lenguaje humano de lo que hace el código, incluyendo cómo maneja los casos de esquina y

  2. El código en sí.

Si el código es mucho más complicado que la descripción, reemplazar el código en línea con una llamada de función puede hacer que el código circundante sea más fácil de entender. Por otro lado, algunos conceptos se pueden expresar de forma más legible en un lenguaje informático que en un lenguaje humano. Consideraría que w=x+y+z; , por ejemplo, es más legible que w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z); .

A medida que una función grande se divide, habrá cada vez menos diferencia entre la complejidad de las subfunciones y sus descripciones, y la ventaja de otras subdivisiones disminuirá. Si las cosas se dividen hasta el punto en que las descripciones serían más complicadas que el código, otras divisiones empeorarán el código.

    
respondido por el supercat 05.06.2017 - 18:45
2

Es un desafío de equilibrio.

Finer es mejor

El método privado proporciona efectivamente un nombre para el código contenido, y algunas veces una firma significativa (a menos que la mitad de sus parámetros sean completamente ad-hoc datos de trampa con interdependencias poco claras e indocumentadas) .

Dar nombres a las construcciones de código es generalmente bueno, siempre y cuando los nombres sugieran un contrato significativo para la persona que llama, y el contrato del método privado corresponda exactamente a lo que sugiere el nombre.

Al obligarse a pensar en contratos significativos para porciones más pequeñas del código, el desarrollador inicial puede detectar algunos errores y evitarlos sin dolor incluso antes de realizar cualquier prueba de desarrollo. Esto solo funciona si el desarrollador se esfuerza por una denominación concisa (sonido simple pero preciso) y está dispuesto a adaptar los límites del método privado para que la denominación concisa sea incluso posible.

El mantenimiento posterior también es útil, ya que los nombres adicionales ayudan a que el código se autodocumente .

  • Contratos sobre pequeñas piezas de código
  • Los métodos de nivel superior a veces se convierten en una secuencia corta de llamadas, cada una de las cuales hace algo significativo y con un nombre distinto, acompañada de la capa delgada de manejo de errores general y más externo. Dicho método puede convertirse en un recurso de capacitación valioso para cualquier persona que tenga que convertirse rápidamente en un experto en la estructura general del módulo.

Hasta que se ponga demasiado fino

¿Es posible exagerar el dar nombres a pequeños trozos de código y terminar con demasiados métodos privados demasiado pequeños? Por supuesto. Uno o más de los siguientes síntomas indican que los métodos son demasiado pequeños para ser útiles:

  • Demasiadas sobrecargas que no representan realmente firmas alternativas para la misma lógica esencial, sino más bien una única pila de llamadas fija.
  • Los sinónimos se usan solo para referirse al mismo concepto repetidamente ("denominación entretenida")
  • Un montón de decoraciones de nombres de carácter superficial, como XxxInternal o DoXxx , especialmente si no hay un esquema unificado para presentarlos.
  • Nombres torpes casi más largos que la implementación misma, como LogDiskSpaceConsumptionUnlessNoUpdateNeeded
respondido por el Jirka Hanika 08.06.2017 - 09:26
1

Contrariamente a lo que han dicho los demás, yo diría que un método público largo es un olor de diseño que se not rectifica al descomponerse en métodos privados.

  

Pero a veces tengo un gran método público que podría dividirse en pasos más pequeños

Si este es el caso, argumentaría que cada paso debe ser su propio ciudadano de primera clase con una responsabilidad única para cada uno. En un paradigma orientado a objetos, sugeriría crear una interfaz y una implementación para cada paso de tal manera que cada uno tenga una responsabilidad única, fácilmente identificable y se pueda nombrar de manera que la responsabilidad sea clara. Esto le permite realizar una prueba unitaria del método público (anteriormente) de gran tamaño, así como cada paso individual de forma independiente. Todos deberían estar documentados también.

¿Por qué no descomponerse en métodos privados? Aquí hay algunas razones:

  • acoplamiento apretado y probabilidad. Al reducir el tamaño de su método público, ha mejorado su legibilidad, pero todo el código aún está estrechamente unido. Puede realizar pruebas unitarias de los métodos privados individuales (utilizando las características avanzadas de un marco de prueba), pero no puede probar fácilmente el método público independientemente de los métodos privados. Esto va en contra de los principios de las pruebas unitarias.
  • Tamaño y complejidad de la clase. Ha reducido la complejidad de un método, pero ha aumentado la complejidad de la clase. El método público es más fácil de leer, pero ahora la clase es más difícil de leer porque tiene más funciones que definen su comportamiento. Mi preferencia es para clases pequeñas de responsabilidad única, por lo que un método largo es una señal de que la clase está haciendo demasiado.
  • No se puede reutilizar fácilmente. A menudo es el caso que a medida que un cuerpo de código madura, la reutilización es útil. Si sus pasos están en métodos privados, no se pueden reutilizar en ningún otro lugar sin extraerlos primero de alguna manera. Además, puede animar a copiar y pegar cuando se necesita un paso en otro lugar.
  • Es probable que la división de esta manera sea arbitraria. Yo diría que dividir un método público largo no toma tanto en consideración o diseño como si fuera a dividir las responsabilidades en clases. Cada clase debe estar justificada con un nombre propio, documentación y pruebas, mientras que un método privado no recibe tanta consideración.
  • Oculta el problema. Así que has decidido dividir tu método público en pequeños métodos privados. Ahora no hay problema! ¡Puedes seguir agregando más y más pasos agregando más y más métodos privados! Por el contrario, creo que este es un problema importante. Establece un patrón para agregar complejidad a una clase que será seguido por subsiguientes correcciones de errores e implementaciones de características. Pronto tus métodos privados crecerán y ellos tendrán que dividirse.
  

pero me preocupa que forzar a cualquiera que lea el método para saltar a diferentes métodos privados dañará la legibilidad

Este es un argumento que he tenido con uno de mis colegas recientemente. Argumenta que tener el comportamiento completo de un módulo en el mismo archivo / método mejora la legibilidad. Estoy de acuerdo en que el código es más fácil de seguir cuando están todos juntos, pero el código es menos fácil de razonar sobre a medida que aumenta la complejidad. A medida que un sistema crece, se vuelve intratable al razonar sobre el módulo completo en su totalidad. Cuando descompones la lógica compleja en varias clases, cada una con una sola responsabilidad, entonces es mucho más fácil razonar sobre cada parte.

    
respondido por el Samuel 07.06.2017 - 05:40

Lea otras preguntas en las etiquetas