Cómo mantener diferentes versiones personalizadas del mismo software para múltiples clientes

46

Tenemos múltiples clientes con diferentes necesidades. Aunque nuestro software está modularizado hasta cierto punto, es casi seguro que necesitamos ajustar un poco la lógica comercial de cada módulo para cada cliente. Los cambios son probablemente demasiado pequeños para justificar la división del módulo en un módulo (físico) distinto para cada cliente, me temo que haya problemas con la compilación, un caos de enlaces. Sin embargo, esos cambios son demasiado grandes y demasiados para configurarlos mediante conmutadores en algún archivo de configuración, ya que esto generaría problemas durante la implementación y probablemente muchos problemas de soporte, especialmente con los administradores de tipo tinker.

Me gustaría que el sistema de compilación cree varias compilaciones, una para cada cliente, donde los cambios están contenidos en una versión especializada del módulo físico en cuestión. Entonces tengo algunas preguntas:

¿Recomendaría dejar que el sistema de compilación cree varias compilaciones? ¿Cómo debo almacenar las diferentes personalizaciones en el control de código fuente, en particular svn?

Halcón
fuente
44
¿ #ifdefFunciona para ti?
ohho
66
Las directivas de preprocesador pueden volverse muy complicadas rápidamente y hacer que el código sea más difícil de leer y más difícil de depurar.
Falcon
1
Debe incluir detalles sobre la plataforma real y el tipo de aplicación (escritorio / web) para obtener mejores respuestas. La personalización en una aplicación de escritorio C ++ es completamente diferente a una aplicación web PHP.
GrandmasterB
2
@Falcon: desde que eligió en 2011 una respuesta sobre la que tengo muchas dudas, ¿puede decirnos si tiene alguna experiencia con el uso de SVN en la forma sugerida en el medio? ¿Son mis objeciones infundadas?
Doc Brown
2
@ Doc Brown: la ramificación y fusión fue tediosa y complicada. En aquel entonces, utilizamos un sistema de complementos con complementos o "parches" específicos del cliente que alteraron el comportamiento o las configuraciones. Tendrá algunos gastos generales, pero se puede administrar con Dependendy Injection.
Falcon el

Respuestas:

7

Lo que necesita es una organización de código similar a la ramificación de características . En su escenario específico, esto debería denominarse ramificación de troncal específica del cliente porque probablemente también utilizará la ramificación de funciones mientras desarrolla cosas nuevas (o resuelve errores).

http://svnbook.red-bean.com/en/1.5/svn.branchmerge.commonpatterns.html

La idea es tener un tronco de código con nuevas ramas de características fusionadas. Me refiero a todas las características no específicas del cliente.

Luego, también tiene sus sucursales específicas del cliente donde también combina las ramas de las mismas características según sea necesario (selección de cerezas si lo desea).

Estas ramas de clientes se parecen a las ramas de características, aunque no son temporales ni de corta duración. Se mantienen durante un período de tiempo más largo y principalmente solo se fusionan. Debe haber el menor desarrollo posible de ramificaciones de características específicas del cliente.

Las ramas específicas del cliente son ramas paralelas al enlace troncal y están activas siempre que el enlace troncal en sí mismo y ni siquiera se fusionan en ningún lugar.

            feature1
            ———————————.
                        \
trunk                    \
================================================== · · ·
      \ client1            \
       `========================================== · · ·
        \ client2            \
         `======================================== · · ·
              \ client2-specific feature   /
               `——————————————————————————´
Robert Koritnik
fuente
77
+1 para la ramificación de funciones. También puede usar una rama para cada cliente. Me gustaría sugerir solo una cosa: use un VCS distribuido (hg, git, bzr) en lugar de SVN / CVS para lograr esto;)
Herberth Amaral
43
-1. Para eso no están las "ramas de características"; contradice su definición como "ramas temporales que se fusionan cuando termina el desarrollo de una característica".
P Shved
14
Todavía tiene que mantener todos esos deltas en control de fuente y mantenerlos alineados, para siempre. A corto plazo, esto podría funcionar. A largo plazo puede ser terrible.
rapid_now
44
-1, este no es un problema de nomenclatura: el uso de diferentes ramas para diferentes clientes es solo otra forma de duplicación de código. Esto es en mi humilde opinión un antipatrón, un ejemplo de cómo no hacerlo.
Doc Brown
77
Robert, creo que entendí bastante bien lo que sugeriste, incluso antes de la edición, pero creo que este es un enfoque horrible. Suponga que tiene N clientes, cada vez que agrega una nueva función principal al tronco, parece que el SCM facilitará la propagación de la nueva función a las N ramas. Pero el uso de ramas de esta manera hace que sea demasiado fácil evitar una separación clara de las modificaciones específicas del cliente. Como resultado, ahora tiene N posibilidades de obtener un conflicto de fusión para cada cambio en el tronco. Además, ahora debe ejecutar N pruebas de integración en lugar de una.
Doc Brown
38

No hagas esto con ramas SCM. Convierta el código común en un proyecto separado que produzca una biblioteca o un artefacto de esqueleto de proyecto. Cada proyecto de cliente es un proyecto separado que luego depende del común como dependencia.

Esto es más fácil si su aplicación base común usa un marco de inyección de dependencias como Spring, para que pueda inyectar fácilmente diferentes variantes de reemplazo de objetos en cada proyecto del cliente, ya que se requieren características personalizadas. Incluso si aún no tiene un marco DI, agregar uno y hacerlo de esta manera puede ser su opción menos dolorosa.

Alba
fuente
Otro enfoque para mantener las cosas simples es usar la herencia (y un solo proyecto) en lugar de usar la inyección de dependencia, manteniendo tanto el código común como las personalizaciones en el mismo proyecto (usando herencia, clases parciales o eventos). Con ese diseño, las sucursales del cliente funcionarían bien (como se describe en la respuesta seleccionada), y uno siempre podría actualizar las sucursales del cliente actualizando el código común.
drizin
Si no me pierdo algo, lo que sugiere es que si tiene 100 clientes, creará (100 x NumberOfChangedProjects ) y usará DI para administrarlos. Si es así, definitivamente me mantendría alejado de este tipo de solución, ya que la mantenibilidad sería terrible ...
desde el
13

Almacene el software para todos los clientes en una sola sucursal. No hay necesidad de divergir los cambios realizados para diferentes clientes. Lo más probable es que desee que su software sea de la mejor calidad para todos los clientes, y la corrección de errores en su infraestructura central debería afectar a todos sin una sobrecarga de fusión innecesaria, lo que también puede introducir más errores.

Modularice el código común e implemente el código que difiere en diferentes archivos o guárdelo con diferentes definiciones. Haga que su sistema de compilación tenga objetivos específicos para cada cliente, cada objetivo compilando una versión con solo el código relacionado con un cliente. Si su código es C, por ejemplo, es posible que desee proteger las características para diferentes clientes con " #ifdef" o cualquier mecanismo que tenga su lenguaje para la gestión de la configuración de compilación, y preparar un conjunto de definiciones correspondientes a la cantidad de características que ha pagado un cliente.

Si no le gustan los ifdefs, use "interfaces e implementaciones", "functors", "archivos de objetos" o cualquier herramienta que su lenguaje proporcione para almacenar diferentes cosas en un solo lugar.

Si distribuye fuentes a sus clientes, es mejor hacer que sus scripts de compilación tengan "objetivos de distribución de fuentes" especiales. Una vez que invocas dicho objetivo, crea una versión especial de las fuentes de tu software, las copia en una carpeta separada para que puedas enviarlas y no las compila.

P Shved
fuente
8

Como muchos han dicho: factorice su código correctamente y personalícelo en función del código común ; esto mejorará significativamente la capacidad de mantenimiento. Ya sea que esté utilizando un lenguaje / sistema orientado a objetos o no, esto es posible (aunque es bastante más difícil de hacer en C que algo que esté realmente orientado a objetos). ¡Este es precisamente el tipo de problema que la herencia y la encapsulación ayudan a resolver!

Michael Trausch
fuente
5

Muy cuidadosamente

La función de ramificación es una opción, pero creo que es algo pesada. También hace que las modificaciones profundas sean fáciles, lo que puede conducir a una bifurcación de su aplicación si no se mantiene bajo control. Idealmente, desea aumentar al máximo las personalizaciones en un intento de mantener su base de código central tan común y genérica como sea posible.

Así es como lo haría, aunque no sé si es aplicable a su base de código sin grandes modificaciones y refactorizaciones. Tuve un proyecto similar donde la funcionalidad básica era la misma pero cada cliente requería un conjunto muy específico de características. Creé un conjunto de módulos y contenedores que luego ensamblo a través de la configuración (a la IoC).

luego, para cada cliente, creé un proyecto que básicamente contiene las configuraciones y el script de compilación para crear una instalación totalmente configurada para su sitio. De vez en cuando coloco allí también algunos componentes personalizados para este cliente. Pero esto es raro y, siempre que sea posible, trato de hacerlo en una forma más genérica y lo empujo hacia abajo para que otros proyectos puedan usarlos.

El resultado final es que obtuve el nivel de personalización que necesitaba, obtuve secuencias de comandos de instalación personalizadas para que cuando llegue al sitio del cliente no parezca que esté modificando el sistema todo el tiempo y, como una ventaja adicional MUY significativa, obtengo para poder crear pruebas de regresión conectadas directamente a la compilación. De esta manera, cada vez que recibo un error específico del cliente, puedo escribir una prueba que reafirmará el sistema a medida que se implementa y, por lo tanto, puede hacer TDD incluso a ese nivel.

en resumen:

  1. Sistema altamente modularizado con una estructura de proyecto plana.
  2. Cree un proyecto para cada perfil de configuración (cliente, aunque más de uno puede compartir un perfil)
  3. Reúna los conjuntos de funciones requeridos como un producto diferente y trátelo como tal.

Si se realiza correctamente, el ensamblaje de su producto debe contener todos menos algunos archivos de configuración.

Después de usar esto por un tiempo, terminé creando metapaquetes que ensamblan los sistemas más utilizados o esenciales como una unidad central y utilizo este metapaquete para ensambles de clientes. Después de unos años terminé teniendo una gran caja de herramientas que pude armar muy rápidamente para crear soluciones para el cliente. Actualmente estoy investigando Spring Roo y veo si no puedo impulsar la idea un poco más, esperando que algún día pueda crear un primer borrador del sistema con el cliente en nuestra primera entrevista ... Supongo que podría llamarlo User Driven Desarrollo ;-).

Espero que esto haya ayudado

Newtopian
fuente
3

Los cambios son probablemente demasiado pequeños para justificar la división del módulo en un módulo (físico) distinto para cada cliente, me temo que haya problemas con la compilación, un caos de enlaces.

En mi opinión, no pueden ser demasiado pequeños. Si es posible, factorizaría el código específico del cliente usando el patrón de estrategia prácticamente en todas partes. Esto reducirá la cantidad de código que debe ser ramificado y reducirá la fusión requerida para mantener el código general sincronizado para todos los clientes. También simplificará las pruebas ... puede probar el código general utilizando estrategias predeterminadas y probar las clases específicas del cliente por separado.

Si sus módulos están codificados de manera que el uso de la política X1 en el módulo A requiere el uso de la política X2 en el módulo B, piense en la refactorización para que X1 y X2 puedan combinarse en una sola clase de política.

Kevin Cline
fuente
1

Puede usar su SCM para mantener sucursales. Mantenga la rama maestra prístina / limpia del código personalizado del cliente. Hacer desarrollo principal en esta rama. Para cada versión personalizada de la aplicación, mantenga sucursales separadas. Cualquier buena herramienta SCM funcionará realmente bien con la fusión de ramas (Git viene a la mente). Cualquier actualización en la rama maestra debe fusionarse en las ramas personalizadas, pero el código específico del cliente puede permanecer en su propia rama.


Sin embargo, si es posible, intente diseñar el sistema de forma modular y configurable. La desventaja de estas ramas personalizadas puede ser que se aleja demasiado del núcleo / maestro.

Htbaa
fuente
1

Si está escribiendo en C simple, aquí hay una forma bastante fea de hacerlo.

  • Código común (por ejemplo, unidad "frangulator.c")

  • código específico del cliente, piezas que son pequeñas y se usan solo para cada cliente.

  • en el código de la unidad principal, use #ifdef y #include para hacer algo como

#ifdef CLIENTE = CLIENTA
#include "frangulator_client_a.c"
#terminara si

Use esto como un patrón una y otra vez en todas las unidades de código que requieren personalización específica del cliente.

Esto es MUY feo, y genera otros problemas, pero también es simple, y puede comparar los archivos específicos del cliente uno contra el otro con bastante facilidad.

También significa que todas las piezas específicas del cliente son claramente visibles (cada una en su propio archivo) en todo momento, y hay una relación clara entre el archivo de código principal y la parte del archivo específica del cliente.

Si se vuelve realmente inteligente, puede configurar archivos MAKE para crear la definición correcta del cliente, de modo que algo como:

hacer clienta

creará para client_a, y "make clientb" creará para client_b y así sucesivamente.

(y "make" sin objetivo proporcionado puede emitir una advertencia o una descripción de uso).

He usado una idea similar antes, lleva un tiempo configurarlo, pero puede ser muy efectivo. En mi caso, un árbol fuente construyó alrededor de 120 productos diferentes.

rápidamente_ahora
fuente
0

En git, la forma en que lo haría es tener una rama maestra con todo el código común y ramas para cada cliente. Cada vez que se realice un cambio en el código central, simplemente vuelva a clasificar todas las ramas específicas del cliente en la parte superior del maestro, de modo que tenga un conjunto de parches móviles para los clientes que se apliquen en la parte superior de la línea base actual.

Siempre que realice un cambio para un cliente y observe un error que debe incluirse en otras ramas, puede seleccionarlo en el maestro o en las otras ramas que necesitan la solución (aunque las diferentes ramas del cliente comparten código) , probablemente deberías hacer que ambos se ramifiquen de una rama común del maestro).

Cercerilla
fuente