¿Cómo puede mi equipo evitar errores frecuentes después de refactorizar?

20

Para darle un poco de historia: trabajo para una empresa con aproximadamente doce desarrolladores de Ruby on Rails (pasantes +/-). El trabajo remoto es común. Nuestro producto está hecho de dos partes: un núcleo bastante grueso y delgado para proyectos de grandes clientes basados ​​en él. Los proyectos de los clientes suelen ampliar el núcleo. La sobrescritura de características clave no ocurre. Podría agregar que el núcleo tiene algunas partes bastante malas que necesitan urgentemente refactorizaciones. Hay especificaciones, pero principalmente para los proyectos del cliente. La peor parte del núcleo no se ha probado (no como debería ser ...).

Los desarrolladores se dividen en dos equipos, trabajando con uno o dos PO para cada sprint. Por lo general, un proyecto de cliente está estrictamente asociado con uno de los equipos y OP.

Ahora nuestro problema: con bastante frecuencia, rompemos las cosas del otro. Alguien del Equipo A expande o refactoriza la característica central Y, causando errores inesperados para uno de los proyectos de clientes del Equipo B. En su mayoría, los cambios no se anuncian en los equipos, por lo que los errores casi siempre son inesperados. El equipo B, incluido el PO, pensó que la característica Y era estable y no la probó antes de su lanzamiento, sin darse cuenta de los cambios.

¿Cómo deshacerse de esos problemas? ¿Qué tipo de 'técnica de anuncio' me puede recomendar?

SDD64
fuente
34
La respuesta obvia es TDD .
Mouviciel
1
¿Cómo es que usted declara que "la sobrescritura de características clave no ocurre", y luego su problema es que sucede? ¿Diferencia en su equipo entre "núcleo" y "características clave", y cómo lo hace? Solo trato de entender la situación ...
logc
44
@mouvciel Eso y no use la escritura dinámica , pero ese consejo en particular llega demasiado tarde en este caso.
Doval
3
Use un lenguaje fuertemente tipado como OCaml.
Cayo
@logc Puede ser que no estaba claro, lo siento. No sobrescribimos una característica central como la biblioteca de filtros en sí, sino que agregamos nuevos filtros a las clases que usamos en nuestros proyectos de clientes. Un escenario común puede ser que los cambios en la biblioteca de filtros destruyan los filtros agregados en el proyecto del cliente.
SDD64

Respuestas:

24

Recomendaría leer Trabajar eficazmente con código heredado de Michael C. Feathers . Explica que realmente necesita pruebas automatizadas, cómo puede agregarlas fácilmente, si aún no las tiene, y qué "código huele" para refactorizar de qué manera.

Además de eso, otro problema central en su situación parece ser la falta de comunicación entre los dos equipos. ¿Qué tan grandes son estos equipos? ¿Están trabajando en diferentes atrasos?

Casi siempre es una mala práctica dividir los equipos de acuerdo con su arquitectura. Por ejemplo, un equipo central y un equipo no central. En cambio, crearía equipos en un dominio funcional, pero de componentes cruzados.

Tohnmeister
fuente
He leído en "The Mythical Man-Month" que la estructura del código generalmente sigue la estructura del equipo / organización. Por lo tanto, esto no es realmente una "mala práctica", sino la forma en que suelen ir las cosas.
Marcel
Creo que en " Dinámica del desarrollo de software ", el gerente detrás de Visual C ++ recomienda vívidamente tener equipos de características; No he leído "The Mythical Man-Month", @Marcel, pero AFAIK enumera las malas prácticas en la industria ...
logc
Marcel, es cierto que esta es la forma en que las cosas van o van, pero cada vez más equipos lo hacen de manera diferente, por ejemplo, equipos de características. Tener equipos basados ​​en componentes resulta en una falta de comunicación cuando se trabaja en características de componentes cruzados. Además de eso, casi siempre dará lugar a discusiones arquitectónicas no basadas en el propósito de una buena arquitectura, sino a personas que tratan de llevar las responsabilidades a otros equipos / componentes. Por lo tanto, obtendrá la situación descrita por el autor de esta pregunta. Consulte también mountaingoatsoftware.com/blog/the-benefits-of-feature-teams .
Tohnmeister
Bueno, hasta donde entendí el OP, declaró que los equipos no están divididos en un equipo central y otro no central. Los equipos se dividen "por cliente", que es esencialmente "por dominio funcional". Y ese es parte del problema: dado que todos los equipos pueden cambiar el núcleo común, los cambios de un equipo afectan al otro.
Doc Brown
@DocBrown Tienes razón. Cada equipo puede cambiar el núcleo. Por supuesto, se supone que esos cambios son beneficiosos para cada proyecto. Sin embargo, trabajan en diferentes atrasos. Tenemos uno para cada cliente y uno para el núcleo.
SDD64
41

La peor parte del núcleo no se ha probado (como debería ser ...).

Este es el problema. La refactorización eficiente depende en gran medida del conjunto de pruebas automatizadas. Si no los tiene, los problemas que está describiendo comienzan a aparecer. Esto es especialmente importante si usa un lenguaje dinámico como Ruby, donde no hay un compilador para detectar errores básicos relacionados con pasar parámetros a métodos.

Eufórico
fuente
10
Eso y refactorizar en pequeños pasos y comprometerse con mucha frecuencia.
Stefan Billiet
1
Probablemente haya muchos consejos que podrían agregar consejos aquí, pero todo se reducirá a este punto. Independientemente de la broma de OP "como debería ser" que muestra que saben que es un problema en sí mismo, el impacto de las pruebas con guiones en la refactorización es inmenso: si un pase se ha convertido en un fracaso, entonces la refactorización no ha funcionado. Si todos los pases siguen siendo pases, entonces la refactorización podría haber funcionado (mover fallas a los pases obviamente sería una ventaja, pero mantener todos los pases como pases es más importante que incluso una ganancia neta; un cambio que rompa una prueba y arregle cinco podría ser un mejora, pero no una refactorización)
Jon Hanna
Le di un "+1", pero creo que las "pruebas automatizadas" no son el único enfoque para resolver esto. Un mejor control de calidad manual, pero sistemático, tal vez por un equipo de control de calidad separado, también podría resolver los problemas de calidad (y probablemente tenga sentido tener ambos: pruebas automáticas y manuales).
Doc Brown
Un buen punto, pero si el núcleo y los proyectos del cliente son módulos separados (y además en un lenguaje dinámico como Ruby), el núcleo puede cambiar tanto una prueba como su implementación asociada , y romper un módulo dependiente sin fallar sus propias pruebas.
logc
Como otros han comentado. TDD. Probablemente ya reconozca que debe realizar pruebas unitarias para la mayor cantidad de código posible. Si bien escribir pruebas unitarias por el simple hecho de ser una pérdida de recursos, cuando comienza a refactorizar cualquier componente, debe comenzar con una extensa prueba de escritura antes de tocar el código central.
jb510
5

Las respuestas anteriores que apuntan a mejores pruebas unitarias son buenas, pero creo que puede haber problemas más fundamentales que abordar. Necesita interfaces claras para acceder al código central desde el código para los proyectos del cliente. De esta manera, si refactoriza el código central sin alterar el comportamiento observado a través de las interfaces , el código del otro equipo no se romperá. Esto hará que sea mucho más fácil saber qué se puede refactorizar de forma "segura" y qué necesita un rediseño, posiblemente una ruptura de la interfaz.

Buhb
fuente
Correcto. Más pruebas automatizadas no traerán más que beneficios y vale la pena hacerlo, pero no resolverá el problema central aquí, que es la falta de comunicación de los cambios centrales. El desacoplamiento envolviendo las interfaces en torno a características importantes será una gran mejora.
Bob Tway
5

Otras respuestas han resaltado puntos importantes (más pruebas unitarias, equipos de características, interfaces limpias para los componentes principales), pero falta un punto, que es el control de versiones.

Si congela el comportamiento de su núcleo haciendo una versión 1 y coloca esa versión en un sistema privado de administración de artefactos 2 , entonces cualquier proyecto del cliente puede declarar su dependencia de la versión principal X , y no se romperá en la próxima versión X + 1 .

La "política de anuncios" se reduce a tener un archivo CAMBIOS junto con cada versión, o tener una reunión de equipo para anunciar todas las características de cada nueva versión principal.

Además, creo que necesita definir mejor qué es "núcleo" y qué subconjunto de eso es "clave". Parece que (correctamente) evita hacer muchos cambios en los "componentes clave", pero permite cambios frecuentes en el "núcleo". Para confiar en algo, debes mantenerlo estable; Si algo no es estable, no lo llame núcleo. ¿Tal vez podría sugerir llamarlo componentes "auxiliares"?

EDITAR : si sigue las convenciones en el sistema de versiones semánticas , cualquier cambio incompatible en la API del núcleo debe estar marcado por un cambio de versión importante . Es decir, cuando cambia el comportamiento del núcleo previamente existente o elimina algo, no solo agrega algo nuevo. Con esa convención, los desarrolladores saben que la actualización de la versión '1.1' a '1.2' es segura, pero pasar de '1.X' a '2.0' es arriesgado y debe revisarse cuidadosamente.

1: Creo que esto se llama gema, en el mundo de Ruby
2: el equivalente a Nexus en Java o PyPI en Python

logc
fuente
La "versión" es importante, de hecho, pero cuando uno intenta resolver el problema descrito congelando el núcleo antes de un lanzamiento, entonces fácilmente termina con la necesidad de una sofisticada ramificación y fusión. El razonamiento es que durante una fase de "compilación de lanzamiento" del equipo A, A podría tener que cambiar el núcleo (al menos para la corrección de errores), pero no aceptará cambios en el núcleo de otros equipos, por lo que terminará con una rama de el núcleo por equipo, que se fusionará "más tarde", que es una forma de deuda técnica. Eso a veces está bien, pero a menudo solo pospone el problema descrito a un punto posterior en el tiempo.
Doc Brown
@DocBrown: estoy de acuerdo con usted, pero escribí bajo el supuesto de que todos los desarrolladores son cooperativos y adultos. Esto no quiere decir que no haya visto lo que usted describe . Pero una parte clave para hacer que un sistema sea confiable es, bueno, luchar por la estabilidad. Además, si el equipo A necesita cambiar X en el núcleo, y el equipo B necesita cambiar X en el núcleo, entonces tal vez X no pertenezca al núcleo; Creo que ese es mi otro punto. :)
logc
@DocBrown Sí, aprendimos a usar una rama del núcleo para cada proyecto de cliente. Esto causó algunos otros problemas. Por ejemplo, no nos gusta "tocar" los sistemas de clientes ya implementados. Como resultado, pueden encontrar varios saltos de versiones menores de su núcleo usado después de cada implementación.
SDD64
@ SDD64: eso es exactamente lo que digo: no integrar los cambios inmediatamente en un núcleo común tampoco es una solución a largo plazo. Lo que necesita es una mejor estrategia de prueba para su núcleo, con pruebas automáticas y manuales también.
Doc Brown
1
Para el registro, no estoy abogando por un núcleo separado para cada equipo, ni niego que se requieran pruebas, pero una prueba central y su implementación pueden cambiar al mismo tiempo, como comenté antes . Solo un núcleo congelado, marcado por una cadena de lanzamiento o una etiqueta de confirmación, puede confiar en un proyecto que se construye sobre él (excluyendo las correcciones de errores y siempre que la política de versiones sea sólida).
logc
3

Como dijeron otras personas, un buen conjunto de pruebas unitarias no resolverá su problema: tendrá problemas al fusionar los cambios, incluso si cada conjunto de pruebas de equipo pasa.

Lo mismo para TDD. No veo cómo puede resolver esto.

Su solución no es técnica. Debe definir claramente los límites "centrales" y asignar un rol de "perro guardián" a alguien, ya sea el desarrollador principal o el arquitecto. Cualquier cambio en el núcleo debe pasar por este perro guardián. Es responsable de asegurarse de que cada salida de todos los equipos se fusionará sin demasiados daños colaterales.

Mathieu Fortin
fuente
Teníamos un "perro guardián", ya que escribió la mayor parte del núcleo. Lamentablemente, también fue responsable de la mayoría de las partes no probadas. Se hizo pasar por YAGNI y fue reemplazado hace medio año por otros dos muchachos. Todavía luchamos por refactorizar esas 'partes oscuras'.
SDD64
2
La idea es tener un conjunto de pruebas unitarias para el núcleo , que es parte del núcleo , con contribuciones de todos los equipos, no conjuntos de pruebas separadas para cada equipo.
Doc Brown
2
@ SDD64: parece confundir "No lo vas a necesitar (todavía)" (lo cual es algo muy bueno) con "No tienes que limpiar tu código (todavía)", lo cual es un hábito extremadamente malo , y en mi humilde opinión todo lo contrario.
Doc Brown
La solución de vigilancia es muy, muy subóptima, en mi humilde opinión. Es como construir un solo punto de falla en su sistema, y ​​además uno muy lento, porque involucra a una persona y a la política. De lo contrario, TDD puede, por supuesto, ayudar con este problema: cada prueba central es un ejemplo para los desarrolladores de proyectos del cliente sobre cómo se supone que se debe usar el núcleo actual. Pero creo que dio su respuesta de buena fe ...
logc
@DocBrown: De acuerdo, tal vez nuestros entendimientos difieran. Las características principales, escritas por él, son demasiado complicadas para satisfacer incluso las posibilidades más extrañas. La mayoría de ellos, nunca nos encontramos. La complejidad nos ralentiza para refactorizarlos, por otro lado.
SDD64
2

Como una solución a más largo plazo, también necesita una comunicación mejor y más oportuna entre los equipos. Cada uno de los equipos que alguna vez utilizarán, por ejemplo, la función central Y, deben participar en la construcción de los casos de prueba planificados para la función. Esta planificación, en sí misma, resaltará los diferentes casos de uso inherentes a la característica Y entre los dos equipos. Una vez que se define cómo debería funcionar la función y se implementan y acuerdan los casos de prueba, se requiere un cambio adicional en su esquema de implementación. El equipo que lanza la función es necesario para ejecutar el caso de prueba, no el equipo que está a punto de usarlo. La tarea, si la hay, que debería causar colisiones, es la adición de un nuevo caso de prueba de cualquiera de los equipos. Cuando un miembro del equipo piensa en un nuevo aspecto de la función que no se prueba, deberían tener la libertad de agregar un caso de prueba que hayan verificado que pasa en su propia caja de arena. De esta manera, las únicas colisiones que ocurrirán serán en el nivel de intención, y deben ser clavadas antes de que la característica refactorizada se libere en la naturaleza.

dolphus333
fuente
2

Si bien cada sistema necesita conjuntos de pruebas efectivos (lo que significa, entre otras cosas, automatización), y si bien estas pruebas, si se usan de manera efectiva, detectarán estos conflictos antes de lo que son ahora, esto no resuelve los problemas subyacentes.

La pregunta revela al menos dos problemas subyacentes: la práctica de modificar el 'núcleo' para satisfacer los requisitos de los clientes individuales, y la falla de los equipos para comunicarse y coordinar su intención de hacer cambios. Ninguna de estas son causas fundamentales, y necesitará comprender por qué se está haciendo esto antes de poder solucionarlo.

Una de las primeras cosas por determinar es si tanto los desarrolladores como los gerentes se dan cuenta de que hay un problema aquí. Si al menos algunos lo hacen, entonces debe averiguar por qué piensan que no pueden hacer nada al respecto, o eligen no hacerlo. Para aquellos que no lo hacen, puede intentar aumentar su capacidad para anticipar cómo sus acciones actuales pueden crear problemas futuros, o reemplazarlos con personas que puedan hacerlo. Hasta que tenga una fuerza de trabajo que sepa cómo van las cosas mal, es poco probable que pueda solucionar el problema (y tal vez ni siquiera entonces, al menos a corto plazo).

Puede ser difícil analizar el problema en términos abstractos, al menos inicialmente, así que concéntrese en un incidente específico que haya resultado en un problema e intente determinar cómo sucedió. Como es probable que las personas involucradas se pongan a la defensiva, deberá estar atento a las justificaciones egoístas y post-hoc para descubrir lo que realmente está sucediendo.

Hay una posibilidad que dudo en mencionar porque es muy poco probable: los requisitos de los clientes son tan dispares que no hay suficientes elementos en común para justificar el código central compartido. Si esto es así, entonces tiene múltiples productos separados, y debe administrarlos como tales, y no crear un acoplamiento artificial entre ellos.

sdenham
fuente
Antes de migrar nuestro producto de Java a RoR, en realidad nos gustó lo que sugirió. Uno tenía un núcleo Java para todos los clientes, pero sus requisitos lo "rompieron" un día y tuvimos que dividirlo. Durante esa situación, enfrentamos problemas como: 'Amigo, el cliente Y tiene una característica central tan agradable. Lástima que no podamos trasladarlo al cliente Z, porque su núcleo es incompatible '. Con Rails, queremos seguir estrictamente una política de "un núcleo para todos". Si tiene que ser así, todavía ofrecemos cambios drásticos, pero esos desapegan al cliente de cualquier actualización adicional.
SDD64
Simplemente llamar a TDD no me parece suficiente. Entonces, además de dividir la sugerencia central, me gusta más su respuesta. Lamentablemente, el núcleo no está perfectamente probado, pero eso no resolvería todos nuestros problemas. Agregar nuevas funciones centrales para un cliente puede parecer perfectamente correcto e incluso darles una construcción ecológica, porque solo las especificaciones básicas se comparten entre los clientes. Uno no se da cuenta, lo que le sucede a cada posible cliente. Por lo tanto, me gusta su sugerencia para descubrir los problemas y hablar sobre lo que los causó.
SDD64
1

Todos sabemos que las pruebas unitarias son el camino a seguir. Pero también sabemos que es realmente difícil adaptarlos de manera realista a un núcleo.

Una técnica específica que puede ser útil para usted al extender la funcionalidad es intentar verificar de manera temporal y local que la funcionalidad existente no ha cambiado. Esto se puede hacer así:

Pseudocódigo original:

def someFunction
   do original stuff
   return result
end

Código de prueba temporal en el lugar:

def someFunctionNew
   new do stuff
   return result
end

def someFunctionOld
   do original stuff
   return result
end

def someFunction
   oldResult = someFunctionOld
   newResult = someFunctionNew
   check oldResult = newResult
   return newResult
end

Ejecute esta versión a través de las pruebas de nivel de sistema que existan. Si todo está bien, sabe que no ha roto las cosas y puede proceder a eliminar el código anterior. Tenga en cuenta que cuando verifica la coincidencia de resultados antiguos y nuevos, también puede agregar código para analizar diferencias para capturar casos que sabe que deberían ser diferentes debido a un cambio previsto, como una corrección de errores.

Keith
fuente
1

"Principalmente, los cambios no se anuncian en los equipos, por lo que los errores casi siempre son inesperados" ¿

Algún problema de comunicación? ¿Qué pasa con (además de lo que todos los demás ya han señalado, que debe realizar pruebas rigurosas) para asegurarse de que haya una comunicación adecuada? ¿Que las personas sean conscientes de que la interfaz en la que escriben va a cambiar en la próxima versión y cuáles serán esos cambios?
Y déles acceso a al menos una interacción ficticia (con implementación vacía) lo antes posible durante el desarrollo para que puedan comenzar a escribir su propio código.

Sin todo eso, las pruebas unitarias no harán mucho, excepto señalar durante las etapas finales que hay algo fuera de control entre las partes del sistema. Quiere saber eso, pero quiere saberlo temprano, muy temprano, y hacer que los equipos hablen entre sí, coordinen esfuerzos y tengan acceso frecuente al trabajo que el otro equipo está haciendo (por lo que se compromete regularmente, no uno masivo comprometerse después de varias semanas o meses, 1-2 días antes del parto).
Su error NO está en el código, ciertamente no en el código del otro equipo que no sabía que estaba jugando con la interfaz en la que están escribiendo. Su error está en su proceso de desarrollo, la falta de comunicación y colaboración entre las personas. El hecho de que estés sentado en habitaciones diferentes no significa que debas aislarte de los otros chicos.

jwenting
fuente
1

Principalmente, tiene un problema de comunicación (probablemente también relacionado con un problema de trabajo en equipo ), por lo que creo que una solución a su caso debería centrarse en ... bueno, la comunicación, en lugar de las técnicas de desarrollo.

Doy por sentado que no es posible congelar o bifurcar el módulo principal al iniciar un proyecto de cliente (de lo contrario, simplemente debe integrar en su empresa los cronogramas de algunos proyectos no relacionados con el cliente que apuntan a actualizar el módulo central).

Así que nos queda la cuestión de tratar de mejorar la comunicación entre los equipos. Esto se puede abordar de dos maneras:

  • con los seres humanos Esto significa que su empresa designa a alguien como el arquitecto del módulo central (o cualquier jerga que sea buena para la alta gerencia) que será responsable de la calidad y disponibilidad del código. Esta persona encarnará el núcleo. Por lo tanto, será compartida por todos los equipos y garantizará una sincronización adecuada entre ellos. Además, también debe actuar como revisora ​​del código comprometido con el módulo central para mantener su coherencia;
  • con herramientas y flujos de trabajo. Al imponer la integración continua en el núcleo, convertirá el código del núcleo en el medio de comunicación. Esto requerirá un poco de esfuerzo primero (mediante la adición de conjuntos de pruebas automatizadas), pero luego los informes nocturnos de CI serán una actualización del estado del módulo central.

Puede encontrar más información sobre CI como proceso de comunicación aquí .

Finalmente, todavía tiene un problema con la falta de trabajo en equipo a nivel de la empresa. No soy un gran admirador de los eventos de team building, pero este parece ser un caso en el que serían útiles. ¿Tiene reuniones periódicas para desarrolladores? ¿Puedes invitar a personas de otros equipos a las retrospectivas de tu proyecto? ¿O tal vez tomar cerveza el viernes por la noche a veces?

sansuiso
fuente