Duplicación de código ilusorio

56

El instinto habitual es eliminar cualquier duplicación de código que vea en el código. Sin embargo, me encontré en una situación donde la duplicación es ilusoria .

Para describir la situación con más detalles: estoy desarrollando una aplicación web, y la mayoría de las vistas son básicamente las mismas: muestran una lista de elementos que el usuario puede desplazarse y elegir, una segunda lista que contiene elementos seleccionados y un "Guardar "para guardar la nueva lista.

Me pareció que el problema es fácil. Sin embargo, cada vista tiene sus propias peculiaridades: a veces es necesario volver a calcular algo, a veces debe almacenar algunos datos adicionales, etc. Esto lo resolví insertando ganchos de devolución de llamada en el código lógico principal.

Hay tantas diferencias mínimas entre las vistas que cada vez es menos mantenible, porque necesito proporcionar devoluciones de llamada para básicamente todas las funciones, y la lógica principal comienza a parecerse a una gran secuencia de invocaciones de devolución de llamada. Al final, no estoy ahorrando tiempo ni código, porque cada vista tiene su propio código que se ejecuta, todo en devoluciones de llamada.

Los problemas son:

  • las diferencias son tan pequeñas que el código se ve casi exactamente igual en todas las vistas,
  • hay tantas diferencias que cuando miras los detalles, codificar no es un poco similar

¿Cómo debo manejar esta situación?
¿Tener una lógica central compuesta completamente de llamadas de devolución de llamada es una buena solución?
¿O debería preferir duplicar el código y descartar la complejidad del código basado en la devolución de llamada?

Mael
fuente
38
Por lo general, me resulta útil dejar ir la duplicación inicialmente. Una vez que tengo algunos ejemplos, es mucho más fácil ver qué es común y qué no, y encontrar una manera de compartir las partes comunes.
Winston Ewert
77
Muy buena pregunta: una cosa a considerar no es solo la duplicación física del código sino la duplicación semántica. Si un cambio en una parte del código necesariamente significa que el mismo cambio se duplicaría en los demás, entonces esa parte es probablemente un candidato para la refactorización o abstracción. A veces puedes normalizarte hasta el punto en que realmente te atrapas, por lo que también consideraría las implicaciones prácticas para tratar la duplicación como semánticamente distintas: pueden ser superadas por las consecuencias de tratar de deduplicar.
Ant P
Recuerde, solo puede reutilizar código que haga lo mismo. Si su aplicación hace cosas diferentes en diferentes pantallas, requerirá diferentes devoluciones de llamada. No hay peros, ni peros al respecto.
corsiKa
13
Mi regla general sobre esto es: si hago un cambio en el código en un lugar, ¿ es un error si no hago exactamente el mismo cambio en todos los demás lugares? Si es así, es el mal tipo de duplicación. Si no estoy seguro, elige lo que sea más legible por ahora. En su ejemplo, las diferencias de comportamiento son intencionales y no se consideran errores, por lo tanto, algunas duplicaciones están bien.
Ixrec
Es posible que le interese leer sobre Programación Orientada a Aspectos.
Ben Jackson

Respuestas:

53

En última instancia, debe tomar una decisión sobre si combinar código similar para eliminar la duplicación.

Parece haber una tendencia desafortunada a tomar principios como "No te repitas" como reglas que deben seguirse de memoria en todo momento. De hecho, estas no son reglas universales sino pautas que deberían ayudarlo a pensar y desarrollar un buen diseño.

Como todo en la vida, debe considerar los beneficios frente a los costos. ¿Cuánto código duplicado se eliminará? ¿Cuántas veces se repite el código? ¿Cuánto esfuerzo será escribir un diseño más genérico? ¿Cuánto es probable que desarrolle el código en el futuro? Y así.

Sin conocer su código específico, esto no está claro. Quizás haya una forma más elegante de eliminar la duplicación (como la sugerida por LindaJeanne). O, tal vez, simplemente no hay suficiente repetición verdadera para garantizar la abstracción.

La atención insuficiente al diseño es una trampa, pero también tenga cuidado con el diseño excesivo.


fuente
Creo que su comentario sobre "tendencias desafortunadas" y seguir ciegamente las pautas está en su lugar.
Mael
1
@Mael ¿Estás diciendo que si no mantienes este código en el futuro, no tendrías buenas razones para obtener el diseño correcto? (sin ofender, solo quiero saber lo que piensas sobre eso)
Visto el
2
@Mael ¡Por supuesto que podríamos considerarlo como un desafortunado cambio de frase! : D Sin embargo, creo que deberíamos ser tan estrictos con nosotros mismos como lo somos con los demás al escribir código (me considero como otro cuando leo mi propio código 2 semanas después de escribirlo).
Visto el
2
@ user61852, entonces no le gustará mucho el Código sin código .
RubberDuck
1
@ user61852, jaja, pero ¿qué pasa si todo depende (de la información no proporcionada en la pregunta)? Pocas cosas son menos útiles que el exceso de certeza.
43

Recuerda que DRY se trata de conocimiento . No importa si dos partes del código se ven similares, idénticas o totalmente diferentes, lo que importa es si se puede encontrar la misma información sobre su sistema en ambas.

Un conocimiento puede ser un hecho ("la desviación máxima permitida del valor deseado es 0.1%") o podría ser algún aspecto de su proceso ("esta cola nunca contiene más de tres elementos"). Es esencialmente cualquier pieza de información codificada en su código fuente.

Entonces, cuando decida si algo es duplicación que debe eliminarse, pregunte si es duplicación de conocimiento. De lo contrario, es probable que sea una duplicación incidental, y extraerlo a un lugar común causará problemas cuando luego desee crear un componente similar donde esa parte aparentemente duplicada sea diferente.

Ben Aaronson
fuente
12
¡Esta! El enfoque de DRY es evitar cambios duplicados .
Matthieu M.
Esto es bastante útil.
1
Pensé que el enfoque de DRY era asegurar que no hubiera dos bits de código que deberían comportarse de manera idéntica pero que no. El problema no es el trabajo duplicado porque los cambios de código deben aplicarse dos veces, el verdadero problema es cuando un cambio de código debe aplicarse dos veces pero no lo es.
gnasher729
3
@ gnasher729 Sí, ese es el punto. Si dos piezas de código tienen una duplicación de conocimiento , entonces esperaría que cuando una necesita cambiar, la otra también necesite cambiar, lo que lleva al problema que usted describe. Si tienen una duplicación incidental , entonces cuando uno necesita cambiar, el otro puede necesitar permanecer igual. En ese caso, si ha extraído un método común (o lo que sea), ahora tiene un problema diferente con el que lidiar
Ben Aaronson el
1
También duplicación esencial y duplicación accidental , vea Un Doppelgänger accidental en Ruby y SECUÉ mi código y ahora es difícil trabajar con él. ¿Que pasó? . Los duplicados accidentales también ocurren en ambos lados de un límite de contexto . Resumen: solo combine duplicados si tiene sentido para sus clientes que estas dependencias se modifiquen simultáneamente .
Eric
27

¿Has considerado usar un patrón de estrategia ? Tendría una clase de Vista que contiene el código común y las rutinas llamadas por varias vistas. Los hijos de la clase View contendrían el código específico de esas instancias. Todos usarían la interfaz común que creó para la Vista y, por lo tanto, las diferencias serían encapsuladas y coherentes.

LindaJeanne
fuente
55
No, no lo he considerado. Gracias por la sugerencia. De una lectura rápida sobre el patrón de Estrategia, parece algo que estoy buscando. Definitivamente voy a investigar más a fondo.
Mael
3
Hay un patrón de método de plantilla . También puedes considerar eso
Shakil el
5

¿Cuál es el potencial de cambio? Por ejemplo, nuestra aplicación tiene 8 áreas de negocios diferentes con un potencial de 4 o más tipos de usuarios para cada área. Las vistas se personalizan según el tipo de usuario y el área.

Inicialmente, esto se hizo usando la misma vista con algunas comprobaciones aquí y allá para determinar si deberían mostrarse diferentes cosas. Con el tiempo, algunas de las áreas de negocios han decidido hacer cosas drásticamente diferentes. Al final, básicamente migramos a una vista (vistas parciales, en el caso de ASP.NET MVC) por pieza de funcionalidad por área de negocio. No todas las áreas de negocios tienen la misma funcionalidad, pero si uno quiere la funcionalidad que tiene otra, esa área tiene su propia vista. Es mucho menos engorroso para la comprensión del código, así como para la capacidad de prueba. Por ejemplo, hacer un cambio para un área no causará un cambio no deseado para otra área.

Como @ dan1111 mencionó, puede ser una decisión judicial. Con el tiempo, puede descubrir si funciona o no.

ps2goat
fuente
2

Un problema podría ser que está proporcionando una interfaz (interfaz teórica, no una función de lenguaje) a un solo nivel de la funcionalidad:

A(a,b,c) //a,b,c are your callbacks or other dependencies

En lugar de múltiples niveles dependiendo de cuánto control se requiera:

//high level
A(a,b,c)
//lower
A myA(a,b)
B(myA,c)
//even lower
A myA(a)
B myB(myA,b)
C myC(myB,c)
//all the way down to you just having to write the code yourself

Según tengo entendido, solo expone la interfaz de alto nivel (A), ocultando los detalles de implementación (las otras cosas allí).

Ocultar los detalles de la implementación tiene ventajas, y usted acaba de encontrar una desventaja: el control es limitado, a menos que agregue explícitamente características para cada cosa que hubiera sido posible al usar directamente las interfaces de bajo nivel.

Así que tienes dos opciones. O solo usa la interfaz de bajo nivel, use la interfaz de bajo nivel porque la interfaz de alto nivel era demasiado trabajo para mantener, o expone las interfaces de alto y bajo nivel. La única opción sensata es ofrecer interfaces de alto y bajo nivel (y todo lo demás), suponiendo que desea evitar el código redundante.

Luego, al escribir otra de sus cosas, observa todas las funcionalidades disponibles que ha escrito hasta ahora (innumerables posibilidades, hasta que usted decida cuáles podrían reutilizarse) y las junta.

Use un solo objeto donde necesite poco control.

Utilice la funcionalidad de nivel más bajo cuando deba ocurrir alguna rareza.

Tampoco es muy blanco y negro. Tal vez su gran clase de alto nivel PUEDE cubrir razonablemente todos los casos de uso posibles. Tal vez los casos de uso varían tanto que no basta con la funcionalidad primitiva de nivel más bajo. Depende de usted para encontrar el equilibrio.

Waterlimon
fuente
1

Ya hay otras respuestas útiles. Agregaré el mío.

La duplicación es mala porque

  1. abarrota el código
  2. abarrota nuestra comprensión del código, pero lo más importante
  3. porque si cambias algo aquí y también tienes que cambiar algo allí , podrías olvidar / introducir errores / ... y es difícil no olvidarlo nunca.

Entonces, el punto es: no estás eliminando la duplicación por el bien o porque alguien dijo que era importante. Lo estás haciendo porque quieres reducir errores / problemas. En su caso, parece que si cambia algo en una vista, probablemente no necesitará cambiar exactamente la misma línea en todas las otras vistas. Entonces tiene una duplicación aparente , no una duplicación real.

Otro punto importante es no volver a escribir desde cero algo que funciona ahora solo por una cuestión de principios, como dijo Joel (ya podría haber oído hablar de él ...). Entonces, si sus puntos de vista están funcionando, proceda a mejorar paso a paso y no caiga en el "peor error estratégico que cualquier compañía de software puede cometer".

Francesco
fuente