¿Siempre es una buena práctica escribir una función para cualquier cosa que necesite repetir dos veces?

129

Yo mismo, no puedo esperar a escribir una función cuando necesito hacer algo más de dos veces. Pero cuando se trata de cosas que solo aparecen dos veces, es un poco más complicado.

Para el código que necesita más de dos líneas, escribiré una función. Pero cuando nos enfrentamos a cosas como:

print "Hi, Tom"
print "Hi, Mary"

Dudo en escribir:

def greeting(name):
    print "Hi, " + name

greeting('Tom')
greeting('Mary')

El segundo parece demasiado, ¿no?

Pero qué pasa si tenemos:

for name in vip_list:
    print name
for name in guest_list:
    print name

Y aquí está la alternativa:

def print_name(name_list):
    for name in name_list:
        print name

print_name(vip_list)
print_name(guest_list)

Las cosas se vuelven difíciles, ¿no? Es difícil decidir ahora.

¿Qué opinas de esto?

    
pregunta Zen 13.01.2015 - 05:15
fuente

15 respuestas

198

Aunque es un factor en la decisión de dividir una función, la cantidad de veces que se repite algo no debería ser el único factor. A menudo tiene sentido crear una función para algo que solo se ejecuta una vez . En general, desea dividir una función cuando:

  • Simplifica cada capa de abstracción individual.
  • Tiene nombres buenos y significativos para las funciones de separación, por lo que generalmente no necesita saltar entre las capas de abstracción para comprender lo que sucede.

Sus ejemplos no cumplen con ese criterio. Vas de una línea a una, y los nombres realmente no te compran nada en términos de claridad. Dicho esto, las funciones que son simples son raras fuera de las tutorías y tareas escolares. La mayoría de los programadores tienden a errar demasiado al otro lado.

    
respondido por el Karl Bielefeldt 13.01.2015 - 06:47
fuente
104

Solo si la duplicación es intencional en lugar de accidental .

O, dicho de otra manera:

Solo si esperabas que evolucionen en el futuro.

He aquí por qué:

A veces, dos piezas de código simplemente pasan para volverse iguales aunque no tengan nada que ver entre sí. En ese caso, must resistirá la tentación de combinarlos, porque la próxima vez que alguien realice el mantenimiento en uno de ellos, esa persona no esperará los cambios se propagan a un llamante que no existía anteriormente, y esa función puede interrumpirse como resultado. Por lo tanto, solo tiene que factorizar el código cuando tenga sentido, no siempre que parezca reducir el tamaño del código.

Regla de oro:

Si el código solo devuelve datos nuevos y no modifica los datos existentes o tiene otros efectos secundarios, entonces es muy probable que sea seguro descartarlos como una función separada. (No puedo imaginar ningún escenario en el que se produjera una ruptura sin cambiar por completo la semántica de la función, en cuyo punto el nombre o la firma de la función también deberían cambiar, y de todos modos debería tener cuidado en ese caso. )

    
respondido por el Mehrdad 14.01.2015 - 09:38
fuente
60

No, no siempre es una buena práctica.

Todas las demás cosas son iguales, el código lineal, línea por línea es más fácil de leer que saltar alrededor de las llamadas a funciones. Una llamada de función no trivial siempre toma parámetros, por lo que debe ordenar todo eso y hacer saltos contextuales mentales de llamada de función a llamada de función. Favorece siempre una mejor claridad de código, a menos que tengas una buena razón para ser poco claro (como obtener las mejoras de rendimiento necesarias).

Entonces, ¿por qué se refacta el código en métodos separados? Para mejorar la modularidad. Para recopilar funcionalidad significativa detrás de un método individual y darle un nombre significativo. Si no está logrando eso, entonces no necesita esos métodos separados.

    
respondido por el Robert Harvey 13.01.2015 - 05:41
fuente
25

En tu ejemplo particular, hacer una función puede parecer una exageración; en cambio, me gustaría preguntar: ¿es posible que este saludo en particular cambie en el futuro? ¿Cómo?

Las funciones no se utilizan simplemente para envolver la funcionalidad y facilitar la reutilización, sino también para facilitar la modificación. Si los requisitos cambian, el código copiado y pegado deberá buscarse y cambiarse manualmente, mientras que con una función, el código solo se debe modificar una vez.

Su ejemplo se beneficiaría de esto no a través de una función, ya que la lógica es casi inexistente, pero posiblemente una constante GREET_OPENING . Esta constante podría luego cargarse desde un archivo para que su programa sea fácilmente adaptable a diferentes idiomas, por ejemplo. Tenga en cuenta que esta es una solución rudimentaria, un programa más complejo probablemente necesitaría una forma más refinada de rastrear i18n, pero nuevamente depende de los requisitos en comparación con el trabajo que hacer.

Al final, se trata de los posibles requisitos y de planificar todo con anticipación para facilitar el trabajo de su futuro.

    
respondido por el Svalorzen 13.01.2015 - 11:31
fuente
22

Personalmente adopté la regla de tres, que justificaré llamando a YAGNI . Si necesito hacer algo una vez escribiré ese código, dos veces solo copiaré / pegaré (sí, ¡acabo de admitir que copiar / pegar!) Porque no lo necesitaré de nuevo, pero si necesito hacer lo mismo Una vez más, lo refaccionaré y extraeré ese fragmento en su propio método, lo he demostrado, para mí mismo, que lo lo volveré a necesitar.

Estoy de acuerdo con lo que han dicho Karl Bielefeldt y Robert Harvey, y mi interpretación de lo que dicen es que la regla primordial es legible. Si hace que mi código sea más fácil de leer, entonces considere crear una función, tenga en cuenta cosas como DRY y SLAP . Si puedo evitar tener un nivel de abstracción cambiante en mi función, creo que es más fácil de manejar en mi cabeza, por lo que no saltar entre funciones (si no puedo entender lo que hace la función simplemente leyendo su nombre) significa menos interruptores en mi procesos mentales.
Del mismo modo, no tengo que cambiar el contexto entre las funciones y el código en línea como print "Hi, Tom" funciona para mí, en este caso podría extraer una función PrintNames() iff el resto de mi La función general era en su mayoría llamadas de función.

    
respondido por el Simon Martin 13.01.2015 - 11:24
fuente
13

Rara vez está bien definido, por lo que debe sopesar las opciones:

  • Fecha límite (arregle la sala de servidores con grabación lo antes posible)
  • La legibilidad del código (puede influir en la elección de cualquier manera)
  • Nivel de abstracción de la lógica compartida (relacionado con lo anterior)
  • Requisito de reutilización (es decir, tener exactamente la misma lógica importante, o simplemente conveniente en este momento)
  • Dificultad de compartir (aquí es donde brilla la pitón, ver más abajo)

En C ++, generalmente sigo la regla de tres (es decir, la tercera vez que necesito lo mismo, lo refactorizo en una pieza que se puede compartir adecuadamente), pero con la experiencia es más fácil hacer esa elección inicialmente, ya que usted sabe más sobre el alcance, el software y amp; dominio con el que estás trabajando.

Sin embargo, en Python es bastante ligero reutilizar, en lugar de repetir, la lógica. Más que en muchos otros idiomas (al menos históricamente), IMO.

Entonces, considere simplemente reutilizar la lógica localmente, por ejemplo, creando una lista a partir de argumentos locales:

def foo():
    for name_list in (vip_list, guest_list): # can be list of tuples, for many args
        for name in name_list:
            print name

Puedes usar una tupla de tuplas y dividirla directamente en el bucle for, si necesitas varios argumentos:

def foo2():
    for header, name_list in (('vips': vip_list), ('people': guest_list)): 
        print header + ": "
        for name in name_list:
            print name

o haga una función local (quizás su segundo ejemplo sea eso), lo que hace que la reutilización de la lógica sea explícita pero también muestra claramente que print_name () no se usa fuera de la función:

def foo():
    def print_name(name_list):
        for name in name_list:
            print name

    print_name(vip_list)
    print_name(guest_list)

Las funciones son preferibles, especialmente cuando es necesario abortar la lógica en el medio (es decir, usar retorno), ya que las interrupciones o las excepciones pueden saturar las cosas innecesariamente.

Cualquiera de los dos es superior a repetir la misma lógica, IMO, y también menos saturación que declarar una función global / clase que solo es utilizada por una persona que llama (aunque dos veces).

    
respondido por el Macke 13.01.2015 - 07:16
fuente
9

Todas las mejores prácticas tienen una razón particular, a la que puede referirse para responder una pregunta de este tipo. Siempre debe evitar las ondulaciones, cuando un posible cambio futuro implica también cambios en otros componentes.

Entonces en tu ejemplo: Si asume que tiene un saludo estándar y quiere asegurarse de que sea el mismo para todas las personas, escriba

def std_greeting(name):
    print "Hi, " + name

for name in ["Tom", "Mary"]:
    std_greeting(name)   # even the function call should be written only once

De lo contrario, tendrías que prestar atención y cambiar el saludo estándar en dos lugares, si sucede que cambia.

Sin embargo, si el "Hola" es el mismo por accidente y cambiar uno de los saludos no necesariamente produce cambios para el otro, entonces manténgalos separados. Por lo tanto, manténgalos separados si hay razones lógicas para creer que el siguiente cambio es más probable:

print "Hi, Tom"
print "Hello, Mary"

Para su segunda parte, decidir cuánto "empaquetar" en funciones probablemente depende de dos factores:

  • mantener los bloques pequeños para facilitar la lectura
  • mantenga los bloques lo suficientemente grandes para que los cambios se produzcan en solo unos pocos bloques. No es necesario agrupar demasiado, ya que es difícil seguir el patrón de llamada.

Idealmente, cuando ocurra un cambio, pensará "Tengo que cambiar la mayor parte del siguiente bloque" y no "Tengo que cambiar el código en algún lugar dentro de un bloque grande".

    
respondido por el Gerenuk 13.01.2015 - 14:53
fuente
5

El aspecto más importante de las constantes y funciones nombradas no es tanto que reduzcan la cantidad de escritura, sino que "adjunten" los diferentes lugares donde se usan. Con respecto a su ejemplo, considere cuatro escenarios:

  1. Es necesario cambiar ambos saludos de la misma manera, [por ejemplo, a Bonjour, Tom y Bonjour, Mary ].

  2. Es necesario cambiar un saludo pero dejar el otro como está [por ejemplo. Hi, Tom y Guten Tag, Mary ].

  3. Es necesario cambiar ambos saludos de manera diferente [por ejemplo, Hello, Tom y Howdy, Mary ].

  4. Ninguno de los dos necesita ser cambiado.

Si nunca es necesario cambiar ninguno de los saludos, no importará realmente el enfoque que se tome. El uso de una función compartida tendrá el efecto natural de que cambiar cualquier saludo los cambiará a todos de la misma manera. Si todos los saludos cambiaran de la misma manera, eso sería algo bueno. Sin embargo, si no deberían hacerlo, entonces el saludo de cada persona deberá codificarse o especificarse por separado; Cualquier trabajo realizado para hacer que utilicen una función o especificación común deberá deshacerse (y, en primer lugar, habría sido mejor no hacerlo).

Para estar seguro, no siempre es posible predecir el futuro, pero si uno tiene motivos para creer que es más probable que ambos saludos deban cambiar juntos, ese sería un factor a favor del uso de un código común (o una nombrada constante); Si uno tiene razones para creer que es más probable que uno o ambos deban cambiar para que difieran, eso sería un factor en contra de tratar de usar código común.

    
respondido por el supercat 13.01.2015 - 18:58
fuente
4

Creo que deberías tener una razón para crear un procedimiento. Hay una serie de razones para crear un procedimiento. Los que creo que son los más importantes son:

  1. abstracción
  2. modularidad
  3. navegación de código
  4. consistencia de actualización
  5. recursión
  6. pruebas

Abstracción

Se puede usar un procedimiento para abstraer un algoritmo general de los detalles de pasos particulares en el algoritmo. Si puedo encontrar una abstracción adecuadamente coherente, esto me ayuda a estructurar la forma en que pienso sobre lo que hace el algoritmo. Por ejemplo, puedo diseñar un algoritmo que funcione en listas sin tener que considerar necesariamente la representación de la lista.

Modularidad

Los procedimientos pueden usarse para organizar el código en módulos. Los módulos a menudo se pueden tratar por separado. Por ejemplo, construido y probado por separado.

Los módulos bien diseñados normalmente capturan una unidad de funcionalidad coherente y significativa. Por lo tanto, a menudo pueden ser propiedad de diferentes equipos, reemplazados completamente por una alternativa o reutilizados en un contexto diferente.

Navegación de código

En sistemas grandes, encontrar el código asociado con un comportamiento particular del sistema puede ser un desafío. La organización jerárquica del código dentro de las bibliotecas, los módulos y los procedimientos puede ayudar con este desafío. En particular, si intenta: a) organizar la funcionalidad con nombres significativos y previsibles b) colocar los procedimientos relacionados con una funcionalidad similar cerca uno del otro.

Actualización de consistencia

En sistemas grandes, puede ser difícil encontrar todo el código que se necesita cambiar para lograr un cambio particular en el comportamiento. El uso de procedimientos para organizar la funcionalidad del programa puede hacer esto más fácil. En particular, si cada bit de funcionalidad de su programa aparece solo una vez y en un grupo cohesivo de procedimientos es menos probable (según mi experiencia) que se pierda en algún lugar en el que debería haber actualizado o realizado una actualización inconsistente.

Tenga en cuenta que debe organizar procedimientos basados en la funcionalidad y las abstracciones del programa. No se basa en si dos bits de código son iguales en este momento.

Recursión

El uso de la recursión requiere que crees procedimientos

Pruebas

Por lo general, puede probar los procedimientos de forma independiente. Probar diferentes partes del cuerpo de un procedimiento de forma independiente es más difícil, ya que normalmente tiene que ejecutar la primera parte del procedimiento antes de la segunda parte. Esta observación también se aplica a menudo a otros enfoques para especificar / verificar el comportamiento de los programas.

Conclusión

Muchos de estos puntos se relacionan con la comprensibilidad del programa. Podría decir que los procedimientos son una forma de crear un nuevo idioma, específico para su dominio, con el cual organizar, escribir y leer, acerca de los problemas y procesos en su dominio.

    
respondido por el flamingpenguin 15.01.2015 - 16:10
fuente
4

Ninguno de los casos que das parece que vale la pena refactorizar.

En ambos casos, está realizando una operación que se expresa de manera clara y sucinta, y no existe peligro de que se realice un cambio inconsistente.

Te recomiendo que siempre busques formas de aislar el código donde:

  1. Hay una tarea identificable y significativa que podría separar, y

  2. Cada uno de ellos

    a. la tarea es compleja de expresar (teniendo en cuenta las funciones disponibles para usted) o

    b. la tarea se realiza más de una vez, y necesita una forma de asegurarse de que se cambie de la misma manera en ambos lugares,

Si la condición 1 no se cumple, no encontrará la interfaz correcta para el código: si intenta forzarla, terminará con muchos parámetros, varias cosas que desea devolver, mucha lógica contextual y muy posiblemente no pueda encontrar un buen nombre. Primero, intente documentar la interfaz en la que está pensando, es una buena manera de probar la hipótesis de que es el conjunto de funciones correcto y viene con la ventaja adicional de que cuando escribe su código, ¡ya está especificado y documentado!

2a es posible sin 2b: a veces he eliminado el código incluso cuando sé que solo se usa una vez, simplemente porque moverlo a otro lugar y reemplazarlo con una sola línea significa que el contexto en el que se llama es repentinamente Mucho más legible (especialmente si es un concepto fácil que el lenguaje hace que sea difícil de implementar). También es claro cuando se lee la función extraída donde la lógica comienza y termina y lo que hace.

2b es posible sin 2a: el truco es tener una idea de qué tipo de cambios son más probables. ¿Es más probable que la política de su empresa cambie de decir "Hola" a "Hola" todo el tiempo o que su saludo cambie a un tono más formal si está enviando una demanda de pago o una disculpa por una interrupción del servicio? Algunas veces puede ser porque está usando una biblioteca externa de la que no está seguro y desea poder cambiarla rápidamente por otra: la implementación puede cambiar incluso si la funcionalidad no lo hace.

La mayoría de las veces, sin embargo, tendrás una mezcla de 2a y 2b. Y tendrá que usar su criterio, teniendo en cuenta el valor comercial del código, la frecuencia con la que se usa, si está bien documentado y comprendido, el estado de su conjunto de pruebas, etc.

Si observa que se utiliza el mismo bit de lógica más de una vez, debe aprovechar la oportunidad para considerar la refactorización. Si está comenzando nuevas declaraciones con más de n niveles de sangría en la mayoría de los idiomas, ese es otro activador, un poco menos significativo (elija un valor de n con el que se sienta cómodo para su idioma dado: Python podría ser 6, para instancia).

Mientras estés pensando en estas cosas y derribando a los grandes monstruos con código de espagueti, deberías estar bien, no te preocupes tanto por lo delicado que debe ser tu refactorización y no lo hagas. No tengo tiempo para cosas como pruebas o documentación elaborada. Si es necesario hacerlo, el tiempo lo dirá.

    
respondido por el user52889 13.01.2015 - 23:08
fuente
3

En general estoy de acuerdo con Robert Harvey, pero quería agregar un caso para dividir una funcionalidad en funciones. Para mejorar la legibilidad. Considere un caso:

def doIt(smth,smthElse)
    for x in getDataFromSomething(smth,smthElse):
        if not check(x,smth):
            continue
        process(x,smthElse)
        store(x) 

A pesar de que esas llamadas a funciones no se usan en ningún otro lugar, hay una ganancia significativa en la legibilidad si las 3 funciones son considerablemente largas y tienen bucles anidados, etc.

    
respondido por el UldisK 13.01.2015 - 09:12
fuente
2

No hay una regla dura y rápida que deba aplicarse, dependerá de la función real que se use. Para la impresión simple de nombres, no usaría una función, pero si es una suma matemática sería una historia diferente. Luego, crearía una función incluso si solo se llama dos veces, esto es para asegurar que la suma de las matemáticas sea siempre la misma si alguna vez se cambia. En otro ejemplo, si está haciendo cualquier forma de validación, usaría una función, por lo que en su ejemplo, si necesita verificar que el nombre tiene más de 5 caracteres, usaría una función para garantizar que siempre se realicen las mismas validaciones.

Entonces, creo que respondiste a tu propia pregunta cuando dijiste "Para los códigos que necesitan más de dos líneas, debo escribir una función". En general, estaría usando una función, pero también debe usar su propia lógica para determinar si existe algún tipo de valor agregado al usar una función.

    
respondido por el W.Walford 13.01.2015 - 05:52
fuente
2

He llegado a creer algo acerca de la refactorización que no he visto mencionada aquí, sé que ya hay muchas respuestas aquí, pero creo que esto es nuevo.

He sido un refactorista despiadado y un fuerte creyente en la SECA desde antes de que surgieran los términos. Principalmente es porque tengo problemas para mantener una base de código grande en mi cabeza y en parte porque disfruto de la codificación DRY y no disfruto nada de la codificación C & P, de hecho es doloroso y terriblemente lento para mí.

La cuestión es que insistir en DRY me ha dado mucha práctica en algunas técnicas que rara vez veo que otras usan. Mucha gente insiste en que es difícil o imposible hacer Java en DRY, pero en realidad no lo intentan.

Un ejemplo de hace mucho tiempo que es algo similar a su ejemplo. La gente tiende a pensar que la creación de Java GUI es difícil. Por supuesto que si escribes así:

Menu m=new Menu("File");
MenuItem save=new MenuItem("Save")
save.addAction(saveAction); // I forget the syntax, but you get the idea
m.add(save);
MenuItem load=new MenuItem("Load")
load.addAction(loadAction)

Cualquiera que piense que esto es una locura tiene toda la razón, pero no es culpa de Java: el código nunca debe escribirse de esta manera. Estas llamadas de método son funciones destinadas a envolverse en otros sistemas. Si no puede encontrar un sistema de este tipo, ¡constrúyalo!

Obviamente no puedes codificar así, así que necesitas dar un paso atrás y mirar el problema, ¿qué está haciendo realmente ese código repetido? Está especificando algunas cadenas y su relación (un árbol) y uniendo las hojas de ese árbol a las acciones. Así que lo que realmente quieres es decir:

class Menu {
    @MenuItem("File|Load")
    public void fileLoad(){...}
    @MenuItem("File|Save")
    public void fileSave(){...}
    @MenuItem("Edit|Copy")
    public void editCopy(){...}...

Una vez que haya definido su relación de alguna manera que sea sucinta y descriptiva, escriba un método para tratarla. En este caso, repita los métodos de la clase pasada y construya un árbol y luego use eso para construye tus menús y acciones, así como (obviamente) muestra un menú. No tendrá duplicación, y su método es reutilizable ... y probablemente más fácil de escribir que un gran número de menús, y si realmente disfruta de la programación, se divirtió MUCHO más. Esto no es difícil, ¡el método que necesita para escribir es probablemente menos líneas que crear el menú a mano sería!

La cosa es que, para hacer esto bien, necesitas practicar, mucho. Debe ser bueno analizando exactamente qué información única hay en las partes repetidas, extraiga esa información y descubra cómo expresarla bien. Aprender a usar herramientas como el análisis de cadenas y las anotaciones ayuda mucho. Aprender a ser realmente claro sobre el informe de errores y la documentación también es muy importante.

Obtienes la práctica "gratis" simplemente mediante la codificación correcta. Lo más probable es que una vez que lo hagas, encuentres que la codificación de algo SECO (incluida la escritura de una herramienta reutilizable) es más rápida que copiar y pegar, y todos los errores , errores duplicados y cambios difíciles que causa el tipo de codificación.

No creo que pueda disfrutar de mi trabajo si no practicara las técnicas DRY y las herramientas de construcción tanto como podría. Si tuviera que reducir mi pago para no tener que copiar y pegar la programación, lo tomaría.

Así que mis puntos son:

  • Copiar & Pegar cuesta más tiempo a menos que no sepa refactorizar bien.
  • Aprendes a refactorizar bien haciéndolo, insistiendo en DRY incluso en los casos más difíciles y triviales.
  • Eres un programador, si necesitas una pequeña herramienta para hacer tu código DRY, compílalo.
respondido por el Bill K 15.01.2015 - 05:41
fuente
1

En general, es una buena idea dividir algo en una función para mejorar la legibilidad de su código, o, si la parte que se repite a menudo es de alguna manera esencial para el sistema. En el ejemplo anterior, si necesita saludar a sus usuarios según la configuración regional, entonces tendría sentido tener una función de saludo independiente.

    
respondido por el user90766 04.03.2015 - 14:26
fuente
1

Como corolario del comentario de @DocBrown a @RobertHarvey sobre el uso de funciones para crear abstracciones: si no puede encontrar un nombre de función adecuadamente informativo que no sea significativamente más conciso o claro que el código detrás de él, es No abstraemos mucho. A falta de cualquier otra buena razón para convertirla en una función, no hay necesidad de hacerlo.

Además, el nombre de una función rara vez captura su semántica completa, por lo que es posible que tenga que revisar su definición, si no es lo suficientemente familiar como para estar familiarizado. Particularmente en otro lenguaje que no sea funcional, es posible que necesite saber si tiene efectos secundarios, qué errores pueden ocurrir y cómo se responden, su complejidad de tiempo, si asigna y / o libera recursos, y si es un hilo conductor. seguro.

Por supuesto, por definición estamos considerando solo funciones simples aquí, pero eso va en ambos sentidos: en tales casos, es probable que la incorporación no agregue complejidad. Además, el lector probablemente no se dará cuenta de que se trata de una función simple hasta que mira. Incluso con un IDE que lo vincula con la definición, el salto visual es un impedimento para la comprensión.

En términos generales, el factoraje recursivo en más funciones más pequeñas eventualmente lo llevará al punto donde las funciones ya no tienen un significado independiente, ya que a medida que los fragmentos de código de los que están hechos se hacen más pequeños, esos fragmentos obtienen más de su significado de El contexto creado por el código que lo rodea. Como analogía, piense en ello como una imagen: si se acerca demasiado, no puede distinguir lo que está viendo.

    
respondido por el sdenham 14.01.2015 - 17:53
fuente

Lea otras preguntas en las etiquetas