¿Son los métodos privados con un solo estilo malo de referencia?

139

En general, uso 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 acortaría el método público, pero me preocupa que obligar a cualquiera que lea el método a saltar a diferentes métodos privados dañará la legibilidad.

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

Jordak
fuente
77
Todas las respuestas están basadas en opiniones. Los autores originales siempre piensan que su código de raviolis es más legible; los editores posteriores lo llaman ravioles.
Frank Hileman
55
Te sugiero que leas Código limpio. Recomienda este estilo y tiene muchos ejemplos. También tiene una solución para el problema de "saltar".
Kat
1
Creo que esto depende de su equipo y qué líneas de código están contenidas.
HopefulHelpful
1
¿No se basa todo el marco de STRUTS en métodos Getter / Setter de un solo uso, todos los cuales son métodos de un solo uso?
Zibbobz

Respuestas:

203

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 ciertamente una buena razón para crearlos, pero hay otra: descomposición.

Considere una función que hace demasiado. Tiene cien líneas de largo e imposible de razonar.

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: hacer A, luego hacer B, luego hacer C, etc. Las funciones que llama solo pueden llamarse en un lugar, pero ahora son más pequeñas. Cualquier función particular está necesariamente aislada de las otras funciones: tienen diferentes ámbitos.

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

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

  • Localidad de referencia. Ahora se vuelve imposible declarar y usar una variable, luego hacer que se quede y volver a usar 100 líneas más tarde. Estas funciones tienen diferentes ámbitos.

  • Pruebas. Si bien solo es necesario evaluar unitariamente a los miembros públicos de una clase, también puede ser conveniente evaluar 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 probarla 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. Para 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 piezas más pequeñas que sean más fáciles de entender y probar es un punto clave del libro Clean Code del tío Bob . Al momento de escribir esta respuesta, el libro tenía nueve años, pero hoy es tan relevante como lo era en aquel entonces.


fuente
77
No olvides mantener niveles consistentes de abstracción y buenos nombres que aseguren que mirar dentro de los métodos no te sorprenderá. No se sorprenda si un objeto completo se esconde allí.
candied_orange
14
A veces, los métodos de división pueden ser buenos, pero mantener las cosas en línea también puede tener ventajas. Si una verificación de límites se pudiera hacer de manera sensata dentro o fuera de un bloque de código, por ejemplo, mantener ese bloque de código en línea facilitará ver si la verificación de límites se realiza una vez, más de una vez o no . Tirar del bloque de código en su propia subrutina haría que tales cosas sean más difíciles de determinar.
supercat
77
La viñeta de "legibilidad" podría etiquetarse como "Abstracción". Se trata de la abstracción. Los "trozos pequeños" hacen una cosa , una cosa de bajo nivel que realmente no te importan los detalles esenciales para cuando estás leyendo o entrando en el método público. Al abstraerlo a una función, puede pasar por encima y centrarse en el gran esquema de cosas / lo que el método público está haciendo en un nivel superior.
Mathieu Guindon
77
La bala de "prueba" es cuestionable en mi opinión. Si sus miembros privados quieren ser evaluados, entonces probablemente pertenezcan a su propia clase, como miembros públicos. Por supuesto, el primer paso para extraer una clase / interfaz es extraer un método.
Mathieu Guindon
66
Dividir una función puramente por números de línea, bloques de código y alcance variable sin preocuparse por la funcionalidad real es una idea terrible, y diría que la mejor alternativa es dejarla en una sola función. Debería dividir las funciones según la funcionalidad. Ver la respuesta de DaveGauer . De alguna manera estás insinuando esto en tu respuesta (con menciones de nombres y problemas), pero no puedo evitar sentir que este aspecto necesita mucho más enfoque, dada su importancia y relevancia en relación con la pregunta.
Dukeling
36

¡Probablemente sea una gran idea!

No estoy de acuerdo con dividir largas secuencias lineales de acción en funciones separadas simplemente para reducir la longitud promedio de la función en su 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 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 propósito claro ( incluso si se llama solo una vez ), entonces ha 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 nombres verbales agradables y cuando la función principal los llama y todo se lee prácticamente como un párrafo de prosa.

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

DaveGauer
fuente
2
He escuchado este argumento durante muchos años, y nunca se desarrolla en el mundo real. Estoy hablando específicamente de una o dos funciones de línea, llamadas desde un solo lugar. Mientras que el autor original siempre piensa que es "más legible", los editores posteriores a menudo lo llaman código de ravioles. Esto depende de la profundidad de la llamada, generalmente hablando.
Frank Hileman
34
@FrankHileman Para ser justos, nunca vi a ningún desarrollador complementar el código de su antecesor.
T. Sar
44
¡@TSar, el desarrollador anterior, es la fuente de todos los problemas!
Frank Hileman
18
@TSar Nunca he felicitado al desarrollador anterior cuando era el desarrollador anterior.
IllusiveBrian
3
Lo bueno de la descomposición es que puedes componer foo = compose(reglorbulate, deglorbFramb, getFrambulation);
:;
19

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

  • 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 entre sí. 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 importan. Si no logra capturar 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 qué funciones públicas lo llaman. Esto puede conducir 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 otros. Esto no significa que todavía no pueda ser claro para los demás, pero es fácil decir que tiene mucho sentido que bar()deba llamarse antes foo()en el momento de la escritura.

  • Reutilización peligrosa. Su función pública probablemente restringió qué entrada fue posible para cada función privada. Si estos supuestos de entrada no se capturan correctamente (documentar TODOS los supuestos es difícil), alguien puede reutilizar incorrectamente una de sus funciones privadas e introducir errores en la base de código.

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

dlasalle
fuente
66
Ignore la legibilidad, echemos un vistazo al aspecto de informe de errores: si el usuario informa que ocurrió un error MainMethod(lo suficientemente fácil de obtener, generalmente se incluyen símbolos y debe tener un archivo de desreferencia de símbolos) que es de 2k líneas (los números de línea nunca se incluyen en informes de errores) y es un NRE que puede suceder en 1k de esas líneas, bueno, estaré condenado si estoy investigando eso. Pero si me dicen que sucedió SomeSubMethodAy son 8 líneas y la excepción fue una NRE, entonces probablemente encontraré y arreglaré eso lo suficientemente fácil.
410_Gone
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 de 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 facilitar la comprensión del código circundante. Por otro lado, algunos conceptos se pueden expresar de manera más legible en un lenguaje de computadora que en un lenguaje humano. Consideraría w=x+y+z;, por ejemplo, que es más legible que w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);.

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

Super gato
fuente
2
El nombre de su función es engañoso. No hay nada en w = x + y + z que indique ningún tipo de control de desbordamiento, mientras que el nombre del método en sí mismo parece implicar lo contrario. Un nombre mejor para su función sería "addAll (x, y, z)", por ejemplo, o incluso "Sum (x, y, z)".
T. Sar
66
Dado que estamos hablando de métodos privados , esta pregunta posiblemente no se refiera a C. De todos modos, ese no es mi punto: simplemente podría llamar a la misma función "suma" sin la necesidad de abordar la suposición en la firma del método. Según su lógica, cualquier método recursivo debería llamarse "DoThisAssumingStackDoesntOverflow", por ejemplo. Lo mismo ocurre con "FindSubstringAssumingStartingPointIsInsideBounds".
T. Sar
1
En otras palabras, los supuestos básicos, sean cuales sean (dos números no se desbordarán, la pila no se desbordará, el índice se pasó correctamente) no deberían estar presentes en el nombre del método. Esas cosas deben documentarse, por supuesto, si es necesario, pero no en la firma del método.
T. Sar
1
@TSar: Estaba siendo un poco gracioso con el nombre, pero el punto clave es que (cambiando al promedio porque es un mejor ejemplo) un programador que vea "(x + y + z + 1) / 3" en contexto será mucho mejor ubicado para saber si esa es una manera apropiada de calcular el promedio dado el código de llamada, que alguien que está buscando la definición o el sitio de la llamada int average3(int n1, int n2, int n3). Separar la función "promedio" significaría que alguien que quisiera ver si era adecuado para los requisitos tendría que buscar en dos lugares.
supercat
1
Si tomamos C # por ejemplo, solo tendría un único método "Promedio", que se puede hacer para aceptar cualquier número de parámetros. De hecho, en c #, funciona sobre cualquier colección de números , y estoy seguro de que es mucho más fácil de entender que abordar las matemáticas crudas en el código.
T. Sar
2

Es un desafío de equilibrio.

Más fino es mejor

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

Dar nombres a las construcciones de código es generalmente bueno, siempre que los nombres sugieran un contrato significativo para la persona que llama, y ​​el contrato del método privado corresponde 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 cualquier prueba de desarrollo. Esto solo funciona si el desarrollador se esfuerza por nombrar conciso (un sonido simple pero preciso) y está dispuesto a adaptar los límites del método privado para que el nombre conciso sea incluso posible.

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

  • 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 distintivo, acompañado de una capa delgada de manejo de errores general y externo. Tal método puede convertirse en un valioso recurso de capacitación para cualquiera que tenga que convertirse rápidamente en un experto en la estructura general del módulo.

Hasta que se pone muy bien

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

  • Demasiadas sobrecargas que realmente no representan firmas alternativas para la misma lógica esencial, sino una sola pila de llamadas fija.
  • Los sinónimos se usan solo para referirse al mismo concepto repetidamente ("denominación entretenida")
  • Muchas decoraciones de nombres superficiales como XxxInternalo DoXxx, especialmente si no hay un esquema unificado para introducirlas.
  • Nombres torpes casi más largos que la implementación en sí, como LogDiskSpaceConsumptionUnlessNoUpdateNeeded
Jirka Hanika
fuente
1

Al contrario de lo que han dicho los demás, diría que un método público largo es un olor de diseño que es no se rectifica mediante la descomposición 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, entonces argumentaría que cada paso debe ser su propio ciudadano de primera clase con una única responsabilidad 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 probar el método público grande (anteriormente), así como cada paso individual independientemente uno del otro. Todos deberían estar documentados también.

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

  • Acoplamiento apretado y comprobabilidad. Al reducir el tamaño de su método público, ha mejorado su legibilidad, pero todo el código todavía está estrechamente unido. Puede probar los métodos privados individuales (utilizando las funciones 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 de clase y complejidad. Redujo la complejidad de un método, pero aumentó la complejidad de la clase. El método público es más fácil de leer, pero la clase ahora es más difícil de leer porque tiene más funciones que definen su comportamiento. Prefiero las 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 se da el caso de 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 primero extraerlos de alguna manera. Además, puede alentar 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 pensamiento o consideración de diseño como si dividiera 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. Entonces ha decidido dividir su método público en pequeños métodos privados. Ahora no hay problema! ¡Puede 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 sus métodos privados crecerán y que tendrán que ser dividido.

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

Esta es una discusión que tuve recientemente con uno de mis colegas. 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 es menos fácil razonar sobre el código a medida que crece la complejidad. A medida que un sistema crece, se vuelve intratable razonar sobre el módulo completo como un todo. Cuando descompone la lógica compleja en varias clases, cada una con una sola responsabilidad, resulta mucho más fácil razonar sobre cada parte.

Samuel
fuente
1
Tendría que estar en desacuerdo. Una abstracción excesiva o prematura conduce a un código innecesariamente complejo. Un método privado puede ser el primer paso en un camino que podría necesitar interconectarse y abstraerse, pero su enfoque aquí sugiere deambular por lugares que quizás nunca necesiten visitar.
WillC
1
Entiendo de dónde vienes, pero creo que el código huele a lo que pregunta OP, son signos de que está listo para un mejor diseño. La abstracción prematura sería diseñar estas interfaces incluso antes de que se encuentre con ese problema.
Samuel
Estoy de acuerdo con este enfoque. De hecho, cuando sigues TDD, es la progresión natural. La otra persona dice que es demasiada abstracción prematura, pero me resulta mucho más fácil burlarse rápidamente de una funcionalidad que necesito en una clase separada (detrás de una interfaz), que tener que implementarla en un método privado para que la prueba pase .
Eternal21