¿Cómo evito duplicar código de forma desconocida?

33

Trabajo en una base de código bastante grande. Cientos de clases, toneladas de archivos diferentes, muchas funcionalidades, lleva más de 15 minutos extraer una copia nueva, etc.

Un gran problema con una base de código tan grande es que tiene bastantes métodos de utilidad y que hacen lo mismo, o tiene un código que no usa estos métodos de utilidad cuando podría. Y también los métodos de utilidad no son todos en una sola clase (porque sería un gran desastre).

Soy bastante nuevo en la base del código, pero el líder del equipo que ha estado trabajando en él durante años parece tener el mismo problema. Conduce a una gran cantidad de código y duplicación de trabajo, y como tal, cuando algo se rompe, generalmente se divide en 4 copias del mismo código.

¿Cómo podemos frenar este patrón? Al igual que con la mayoría de los proyectos grandes, no todo el código está documentado (aunque algunos lo están) y no todo el código está ... bueno, limpio. Pero, básicamente, sería realmente bueno si pudiéramos trabajar en mejorar la calidad a este respecto para que en el futuro tuviéramos menos duplicación de código, y cosas como las funciones de utilidad fueran más fáciles de descubrir.

Además, las funciones de utilidad generalmente están en alguna clase auxiliar estática, en alguna clase auxiliar no estática que funciona en un solo objeto, o es un método estático en la clase con el que principalmente "ayuda".

Tuve un experimento al agregar funciones de utilidad como métodos de Extensión (no necesitaba nada interno de la clase, y definitivamente solo se requería en escenarios muy específicos). Esto tuvo el efecto de evitar el desorden en la clase primaria y demás, pero ya no se puede descubrir a menos que ya lo sepas.

Earlz
fuente

Respuestas:

30

La respuesta simple es que realmente no puede evitar la duplicación de código. Sin embargo, puede "arreglarlo" a través de un difícil proceso continuo incremental repetitivo que se reduce a dos pasos:

Paso 1. Comience a escribir pruebas en código heredado (preferiblemente usando un marco de prueba)

Paso 2. Reescribe / refactoriza el código que está duplicado usando lo que has aprendido de las pruebas

Puede usar herramientas de análisis estático para detectar código duplicado y para C # hay muchas herramientas que pueden hacer esto por usted:

Herramientas como esta lo ayudarán a encontrar puntos en el código que haga cosas similares. Continúe escribiendo pruebas para determinar que realmente lo hacen; use las mismas pruebas para simplificar el uso del código duplicado. Esta "refactorización" se puede hacer de varias maneras y puede usar esta lista para determinar la correcta:

Además, también hay un libro completo sobre este tema de Michael C. Feathers, Working Effectively with Legacy Code . Se profundizan las diferentes estrategias que puede tomar para cambiar el código a mejor. Tiene un "algoritmo de cambio de código heredado" que no está lejos del proceso de dos pasos anterior:

  1. Identificar puntos de cambio
  2. Encuentra puntos de prueba
  3. Romper dependencias
  4. Escribir pruebas
  5. Hacer cambios y refactorizar

El libro es una buena lectura si se trata de un desarrollo de campo marrón, es decir, código heredado que debe cambiar.

En este caso

En el caso del OP, puedo imaginar que el código no comprobable es causado por una olla de miel para "métodos y trucos de utilidad" que toman varias formas:

  • métodos estáticos
  • uso de recursos estáticos
  • clases de singleton
  • valores mágicos

Tenga en cuenta que no hay nada de malo en esto, pero por otro lado, generalmente son difíciles de mantener y cambiar. Los métodos de extensiones en .NET son métodos estáticos, pero también son relativamente fáciles de probar.

Sin embargo, antes de continuar con las refactorizaciones, hable con su equipo al respecto. Deben mantenerse en la misma página que usted antes de continuar con cualquier cosa. Esto se debe a que si está refactorizando algo, entonces hay muchas posibilidades de que cause conflictos de fusión. Entonces, antes de reelaborar algo, investígalo, dile a tu equipo que trabaje en esos puntos de código con precaución por un tiempo hasta que hayas terminado.

Dado que el OP es nuevo en el código, hay otras cosas que hacer antes de que debas hacer algo:

  • Tómese el tiempo para aprender de la base de código, es decir, rompa "todo", pruebe "todo", revierta.
  • Pídale a alguien del equipo que revise su código antes de comprometerse. ;-)

¡Buena suerte!

Spoike
fuente
De hecho, tenemos bastantes pruebas de unidad e integración. No es una cobertura del 100%, pero algunas de las cosas que hacemos son casi imposibles de probar unitarias sin cambios radicales en nuestra base de código. Nunca consideré usar el análisis estático para encontrar duplicaciones. Tendré que intentar eso a continuación.
Earlz
@Earlz: ¡El análisis de código estático es increíble! ;-) Además, cada vez que necesite hacer el cambio, piense en soluciones para facilitar los cambios (consulte el refactor del catálogo de patrones para esto)
Spoike
+1 Entendería si alguien ofreciera una recompensa por esta Q para otorgar esta respuesta como "extra útil". El catálogo Refactor to Patterns es dorado, cosas como esta a la manera de GuidanceExplorer.codeplex.com son excelentes ayudas de programación.
Jeremy Thompson
2

También podríamos intentar ver el problema desde otro ángulo. En lugar de pensar que el problema es la duplicación del código, podemos considerar si el problema se origina en la falta de políticas para la reutilización del código.

Hace poco leí el libro Ingeniería de software con componentes reutilizables y, de hecho, tiene un conjunto de ideas muy interesantes sobre cómo fomentar la reutilización del código a nivel de la organización.

El autor de este libro, Johannes Sametinger, describe un conjunto de barreras para la reutilización del código, algunas conceptuales y otras técnicas. Por ejemplo:

Conceptual y técnico

  • Dificultad para encontrar software reutilizable : el software no se puede reutilizar a menos que se pueda encontrar. Es improbable que se vuelva a utilizar cuando un repositorio no tiene suficiente información sobre los componentes o cuando los componentes están mal clasificados.
  • No reutilización del software encontrado : el fácil acceso al software existente no necesariamente aumenta la reutilización del software. Sin querer, el software rara vez se escribe de manera que otros puedan reutilizarlo. Modificar y adaptar el software de otra persona puede ser aún más costoso que programar la funcionalidad necesaria desde cero.
  • Componentes heredados no adecuados para su reutilización : la reutilización de componentes es difícil o imposible a menos que hayan sido diseñados y desarrollados para su reutilización. Simplemente reunir componentes existentes de varios sistemas de software heredados e intentar reutilizarlos para nuevos desarrollos no es suficiente para una reutilización sistemática. La reingeniería puede ayudar a extraer componentes reutilizables, sin embargo, el esfuerzo puede ser considerable.
  • Tecnología orientada a objetos : se cree ampliamente que la tecnología orientada a objetos tiene un impacto positivo en la reutilización de software. Desafortunadamente y erróneamente, muchos también creen que la reutilización depende de esta tecnología o que la adopción de tecnología orientada a objetos es suficiente para la reutilización del software.
  • Modificación : los componentes no siempre serán exactamente como los queremos. Si son necesarias modificaciones, deberíamos poder determinar sus efectos sobre el componente y sus resultados de verificación anteriores.
  • Reutilización de basura : la certificación de componentes reutilizables a ciertos niveles de calidad ayuda a minimizar posibles defectos. Los controles de mala calidad son una de las principales barreras para la reutilización. Necesitamos algunos medios para juzgar si las funciones requeridas coinciden con las funciones proporcionadas por un componente.

Otras dificultades técnicas básicas incluyen

  • Acordar qué constituye un componente reutilizable.
  • Comprender qué hace un componente y cómo usarlo.
  • Comprender cómo interconectar componentes reutilizables con el resto de un diseño.
  • Diseño de componentes reutilizables para que sean fáciles de adaptar y modificar de forma controlada.
  • Organizar un repositorio para que los programadores puedan encontrar y usar lo que necesitan.

Según el autor, se producen diferentes niveles de reutilización dependiendo de la madurez de una organización.

  • Reutilización ad-hoc entre grupos de aplicaciones : si no hay un compromiso explícito de reutilización, entonces la reutilización puede ocurrir de manera informal y casual en el mejor de los casos. La mayor parte de la reutilización, si la hay, ocurrirá dentro de los proyectos. Esto también conduce a la eliminación de código y termina en la duplicación de código.
  • Reutilización basada en repositorio entre grupos de aplicaciones : la situación mejora ligeramente cuando se utiliza un repositorio de componentes y varios grupos de aplicaciones pueden acceder a él. Sin embargo, no existe un mecanismo explícito para colocar componentes en el repositorio y nadie es responsable de la calidad de los componentes en el repositorio. Esto puede generar muchos problemas y dificultar la reutilización del software.
  • Reutilización centralizada con un grupo de componentes.: En este escenario, un grupo de componentes es explícitamente responsable del repositorio. El grupo determina qué componentes se almacenarán en el repositorio y garantiza la calidad de estos componentes y la disponibilidad de la documentación necesaria, y ayuda a recuperar los componentes adecuados en un escenario de reutilización particular. Los grupos de aplicaciones están separados del grupo de componentes, que actúa como una especie de subcontratista para cada grupo de aplicaciones. Un objetivo del grupo de componentes es minimizar la redundancia. En algunos modelos, los miembros de este grupo también pueden trabajar en proyectos específicos. Durante los inicios del proyecto, su conocimiento es valioso para fomentar la reutilización y, gracias a su participación en un proyecto particular, pueden identificar posibles candidatos para su inclusión en el repositorio.
  • Reutilización basada en dominio : la especialización de grupos de componentes equivale a reutilización basada en dominio. Cada grupo de dominio es responsable de los componentes en su dominio, por ejemplo, componentes de red, componentes de interfaz de usuario, componentes de bases de datos.

Entonces, tal vez, además de todas las sugerencias dadas en otras respuestas, podría trabajar en el diseño de un programa de reutilización, involucrar a la administración, formar un grupo de componentes responsable de identificar los componentes reutilizables mediante el análisis de dominio y definir un repositorio de componentes reutilizables que otros desarrolladores puedan fácilmente consultar y buscar soluciones cocinadas a sus problemas.

edalorzo
fuente
1

Hay 2 posibles soluciones:

Prevención : trate de tener la mejor documentación posible. Haga que cada función esté debidamente documentada y sea fácil de buscar en toda la documentación. Además, al escribir código, haga obvio dónde debe ir el código, por lo que es obvio dónde buscar. La cantidad limitada de código de "utilidad" es uno de los puntos clave de esto. Cada vez que escucho "vamos a hacer una clase de utilidad", mi cabello se levanta y mi sangre se congela, porque obviamente es un problema. Siempre tenga una forma rápida y fácil de pedirle a las personas que conozcan la base de código siempre que exista alguna característica.

Solución : si la prevención falla, debería ser capaz de resolver rápida y eficientemente el código problemático. Su proceso de desarrollo debería permitir corregir rápidamente el código duplicado. Las pruebas unitarias son perfectas para esto, ya que puede modificar eficientemente el código sin temor a romperlo. Entonces, si encuentra 2 piezas de código similares, abstraerlas en una función o clase debería ser fácil con un poco de refactorización.

Personalmente, no creo que la prevención sea posible. Cuanto más lo intentes, más problemático será encontrar características ya existentes.

Eufórico
fuente
0

No creo que este tipo de problemas tenga la solución general. El código duplicado no se creará si los desarrolladores tienen la voluntad suficiente para buscar el código existente. También los desarrolladores podrían solucionar los problemas en el acto si lo desean.

Si el lenguaje es C / C ++, la fusión de duplicación será más fácil debido a la flexibilidad de vinculación (se puede llamar a cualquier externfunción sin información previa) Para Java o .NET, es posible que deba idear clases auxiliares y / o componentes de utilidad.

Por lo general, empiezo la eliminación de duplicación del código existente solo si los principales errores surgen de las partes duplicadas.

9dan
fuente
0

Este es un problema típico de un proyecto más grande que ha sido manejado por muchos programadores, que han estado contribuyendo a veces bajo mucha presión de grupo. Es muy tentador hacer una copia de una clase y adaptarla a esa clase específica. Sin embargo, cuando se encuentra un problema en la clase de origen, también debe resolverse en sus descendientes, que a menudo se olvidan.

Hay una solución para esto y se llama Genéricos, que se ha introducido en Java 6. Es el equivalente de C ++ llamado Plantilla. Código del cual aún no se conoce la clase exacta dentro de una clase genérica. Verifique Java Generics y encontrará toneladas y toneladas de documentación para ello.

Un buen enfoque es reescribir el código que parece ser copiado / pegado en muchos lugares reescribiendo el primero que necesita, es decir, corregir debido a un determinado error. Vuelva a escribirlo para usar Generics y también escriba código de prueba muy riguroso.

Asegúrese de invocar todos los métodos de la clase Genérica. También puede introducir herramientas de cobertura de código: el código genérico debe ser una cobertura de código completo porque se usará en varios lugares.

También escriba código de prueba, es decir, utilizando JUnit o similar para la primera clase designada que se utilizará junto con el código genérico.

Comience a usar el código genérico para la segunda versión (la mayoría de las veces) copiada cuando todo el código anterior funcione y esté completamente probado. Verá que hay algunas líneas de código que son específicas para esa Clase designada. Puede llamar a estas líneas codificadas en un método protegido abstracto que necesita ser implementado por la clase derivada que usa la clase base Genérica.

Sí, es un trabajo tedioso, pero a medida que avanza, cada vez será mejor extraer clases similares y reemplazarlo con algo que esté muy limpio, bien escrito y mucho más fácil de mantener.

He tenido una situación similar en la que una clase genérica eventualmente reemplazó algo como 6 o 7 otras clases casi idénticas que eran casi casi idénticas pero que han sido copiadas y pegadas por varios programadores durante un período de tiempo.

Y sí, estoy muy a favor de las pruebas automatizadas del código. Al principio costará más, pero definitivamente le ahorrará una gran cantidad de tiempo en general. E intente lograr una cobertura de código general de al menos 80% y 100% para el código genérico.

Espero que esto ayude y buena suerte.

André van Kouwen
fuente
0

De hecho, voy a hacerme eco de la opinión menos popular aquí y de lado Gangnusy sugerir que la duplicación de código no siempre es dañina y, a veces, podría ser el mal menor.

Si, por ejemplo, me das la opción de usar:

A) Una biblioteca de imágenes estable (inmutable) y pequeña, bien probada , que duplica algunas docenas de líneas de código matemático trivial para matemática vectorial como productos de punto y lerps y pinzas, pero está completamente desacoplada de cualquier otra cosa y se construye en una fracción de un segundo.

B) Una biblioteca de imágenes inestable (que cambia rápidamente) que depende de una biblioteca matemática épica para evitar esa docena de líneas de código mencionadas anteriormente, con la biblioteca matemática inestable y constantemente recibiendo nuevas actualizaciones y cambios, y por lo tanto la biblioteca de imágenes también tiene que ser reconstruido si no se cambia directamente también. Se tarda 15 minutos en limpiar, construir todo.

... entonces, obviamente, debería ser obvio para la mayoría de las personas que A, y en realidad precisamente debido a su duplicación de código menor, es preferible. El énfasis clave que necesito hacer es la parte bien probada . Obviamente, no hay nada peor que tener un código duplicado que ni siquiera funciona en primer lugar, momento en el que está duplicando errores.

Pero también hay que pensar en el acoplamiento y la estabilidad, y algunas duplicaciones modestas aquí y allá pueden servir como un mecanismo de desacoplamiento que también aumenta la estabilidad (naturaleza inmutable) del paquete.

Por lo tanto, mi sugerencia será centrarme más en probar y tratar de llegar a algo realmente estable (como no cambiar, encontrar pocas razones para cambiar en el futuro) y confiable cuyas dependencias de fuentes externas, si las hay, son muy estable, sobre tratar de eliminar todas las formas de duplicación en su código base. En un entorno de equipo grande, este último tiende a ser un objetivo poco práctico, sin mencionar que puede aumentar el acoplamiento y la cantidad de código inestable que tiene en su base de código.


fuente
-2

No olvide que la duplicación de código no siempre es perjudicial. Imagínese: ahora tiene que resolver una tarea en módulos absolutamente diferentes de su proyecto. Justo ahora es la misma tarea.

Podría haber tres razones para ello:

  1. Algunos temas relacionados con esta tarea son los mismos para ambos módulos. En este caso, la duplicación del código es mala y debe liquidarse. Sería inteligente crear una clase o un módulo para admitir este tema y utilizar sus métodos en ambos módulos.

  2. La tarea es teórica en términos de su proyecto. Por ejemplo, es de física o matemáticas, etc. La tarea existe independientemente en su proyecto. En este caso, la duplicación del código es mala y también debe liquidarse. Crearía una clase especial para tales funciones. Y use dicha función en cualquier módulo donde lo necesite.

  3. Pero en otros casos la coincidencia de tareas es una coincidencia temporal y nada más. Sería peligroso creer que estas tareas seguirán siendo las mismas durante los cambios del proyecto debido a la refactorización e incluso a la depuración. En este caso, sería mejor crear dos mismas funciones / piezas de código en diferentes lugares. Y los cambios futuros en uno de ellos no tocarán al otro.

Y este tercer caso ocurre muy a menudo. Si duplica "sin saberlo", principalmente es por esta misma razón: ¡no es una duplicación real!

Por lo tanto, trate de mantenerlo limpio cuando sea realmente necesario y no tema la duplicación si no es necesario.

Gangnus
fuente
2
code duplication is not always harmfulEs un mal consejo.
Tulains Córdova
1
¿Debería inclinarme ante tu autoridad? Había puesto mis razones aquí. Si me estoy equivocando, muestre dónde está el error. Ahora parece más bien su pobre capacidad para mantener la discusión.
Gangnus
3
La duplicación de código es uno de los problemas centrales en el desarrollo de software y muchos científicos y teóricos de la computación han desarrollado paradigmas y metodologías solo para evitar la duplicación de código como la principal fuente de problemas de mantenimiento en el desarrollo de software. Es como decir "escribir un código pobre no siempre es malo", de esa manera cualquier cosa puede justificarse retóricamente. Tal vez tengas razón, pero evitar la duplicación de código es un principio demasiado bueno para vivir para alentar lo contrario ...
Tulains Córdova
Me he puesto aquí argumentos. No tienes La referencia a las autoridades no funcionará desde el siglo XVI. No puede garantizar que los haya entendido correctamente y que también sean autoridades para mí.
Gangnus
Tiene razón, la duplicación de código no es uno de los problemas centrales en el desarrollo de software, y no se han desarrollado paradigmas y metodologías para evitarlo.
Tulains Córdova