Estoy trabajando en un gran proyecto de software altamente personalizado para varios clientes de todo el mundo. Esto significa que tenemos quizás un 80% de código, que es común entre los distintos clientes, pero también mucho código que tiene que cambiar de un cliente a otro. En el pasado hicimos nuestro desarrollo en repositorios separados (SVN) y cuando comenzó un nuevo proyecto (tenemos pocos, pero grandes clientes) creamos otro repositorio basado en cualquier proyecto pasado que tenga la mejor base de código para nuestras necesidades. Esto ha funcionado en el pasado, pero nos encontramos con varios problemas:
- Los errores que se corrigen en un repositorio no se reparan en otros repositorios. Esto podría ser un problema de organización, pero me resulta difícil corregir y parchear un error en 5 repositorios diferentes, teniendo en cuenta que el equipo que mantiene este repositorio podría estar en otra parte del mundo y no tenemos su entorno de prueba , ni conoce su programación ni los requisitos que tienen (un "error" en un país podría ser una "característica" en otro).
- Las características y mejoras realizadas para un proyecto, que también podrían ser útiles para otro proyecto, se pierden o si se usan en otro proyecto a menudo causan grandes dolores de cabeza al fusionarlas de una base de código a otra (ya que ambas ramas podrían haberse desarrollado de forma independiente durante un año) )
- Las refactorizaciones y las mejoras de código realizadas en una rama de desarrollo se pierden o causan más daño que bien si tiene que fusionar todos estos cambios entre las ramas.
Ahora estamos discutiendo cómo resolver estos problemas y hasta ahora se nos ocurrieron las siguientes ideas sobre cómo resolver esto:
Mantenga el desarrollo en ramas separadas pero organícelo mejor al tener un repositorio central donde se fusionen las correcciones de errores generales y hacer que todos los proyectos combinen los cambios de este repositorio central en los suyos de forma regular (por ejemplo, diariamente). Esto requiere una gran disciplina y mucho esfuerzo para fusionarse entre las ramas. Por lo tanto, no estoy convencido de que funcione y podemos mantener esta disciplina, especialmente cuando la presión del tiempo aumenta.
Abandone las ramas de desarrollo separadas y tenga un repositorio de código central donde viva todo nuestro código y realice nuestra personalización al tener módulos conectables y opciones de configuración. Ya estamos utilizando contenedores de Inyección de dependencias para resolver dependencias en nuestro código y estamos siguiendo el patrón MVVM en la mayoría de nuestro código para separar limpiamente la lógica de negocios de nuestra interfaz de usuario.
El segundo enfoque parece ser más elegante, pero tenemos muchos problemas sin resolver en este enfoque. Por ejemplo: cómo maneja los cambios / adiciones en su modelo / base de datos. Estamos usando .NET con Entity Framework para tener entidades fuertemente tipadas. No veo cómo podemos manejar las propiedades que se requieren para un cliente pero que son inútiles para otro cliente sin saturar nuestro modelo de datos. Estamos pensando en resolver esto en la base de datos utilizando tablas satelitales (que tienen tablas separadas donde nuestras columnas adicionales para una entidad específica viven con un mapeo 1: 1 a la entidad original), pero esta es solo la base de datos. ¿Cómo manejas esto en código? Nuestro modelo de datos vive en una biblioteca central que no podríamos ampliar para cada cliente utilizando este enfoque.
Estoy seguro de que no somos el único equipo que lucha con este problema y me sorprende encontrar tan poco material sobre el tema.
Entonces mis preguntas son las siguientes:
- ¿Qué experiencia tiene con un software altamente personalizado, qué enfoque eligió y cómo funcionó para usted?
- ¿Qué enfoque me recomiendan y por qué? ¿Hay un mejor enfoque?
- ¿Hay algún buen libro o artículo sobre el tema que pueda recomendar?
- ¿Tiene recomendaciones específicas para nuestro entorno técnico (.NET, Entity Framework, WPF, DI)?
Editar:
Gracias por todas las sugerencias. La mayoría de las ideas coinciden con las que ya teníamos en nuestro equipo, pero es realmente útil ver la experiencia que tuvo con ellos y consejos para implementarlos mejor.
Todavía no estoy seguro de qué camino tomaremos y no estoy tomando la decisión (solo), pero transmitiré esto en mi equipo y estoy seguro de que será útil.
Por el momento, el tenor parece ser un repositorio único que utiliza varios módulos específicos del cliente. No estoy seguro de que nuestra arquitectura esté a la altura o cuánto tengamos que invertir para que encaje, por lo que algunas cosas podrían vivir en repositorios separados por un tiempo, pero creo que es la única solución a largo plazo que funcionará.
Entonces, gracias de nuevo por todas las respuestas!
Respuestas:
Parece que el problema fundamental no es solo el mantenimiento del repositorio de código, sino la falta de una arquitectura adecuada .
Un marco o biblioteca estándar abarca el primero, mientras que el segundo se implementaría como complementos (complementos, subclases, DI, lo que tenga sentido para la estructura del código).
Un sistema de control de fuente que maneje sucursales y desarrollo distribuido probablemente también ayudaría; Soy fanático de Mercurial, otros prefieren Git. El marco sería la rama principal, cada sistema personalizado sería sub-ramas, por ejemplo.
Las tecnologías específicas utilizadas para implementar el sistema (.NET, WPF, lo que sea) no son importantes en gran medida.
Hacer esto bien no es fácil , pero es crítico para la viabilidad a largo plazo. Y, por supuesto, cuanto más espere, mayor será la deuda técnica con la que tendrá que lidiar.
Puede encontrar útil el libro Arquitectura de software: principios y patrones organizacionales .
¡Buena suerte!
fuente
Una empresa para la que he trabajado tenía el mismo problema, y el enfoque para abordar el problema fue este: se creó un marco común para todos los proyectos nuevos; Esto incluye todo lo que tiene que ser igual en cada proyecto. Por ejemplo, herramientas de generación de formularios, exportación a Excel, registro. Se hizo un esfuerzo para asegurarse de que este marco común solo se mejora (cuando un nuevo proyecto necesita nuevas características), pero nunca se bifurca.
Basado en ese marco, el código específico del cliente se mantuvo en repositorios separados. Cuando es útil o necesario, las correcciones de errores y las mejoras se copian entre proyectos (con todas las advertencias descritas en la pregunta). Sin embargo, las mejoras globalmente útiles entran en el marco común.
Tener todo en una base de código común para todos los clientes tiene algunas ventajas, pero por otro lado, leer el código se vuelve difícil cuando hay innumerables
if
mensajes para hacer que el programa se comporte de manera diferente para cada cliente.EDITAR: Una anécdota para hacer esto más comprensible:
fuente
Uno de los proyectos en los que he trabajado soportó múltiples plataformas (más de 5) en una gran cantidad de lanzamientos de productos. Muchos de los desafíos que está describiendo fueron cosas que enfrentamos, aunque de una manera ligeramente diferente. Teníamos una base de datos patentada, por lo que no tuvimos los mismos tipos de problemas en ese campo.
Nuestra estructura era similar a la suya, pero teníamos un único repositorio para nuestro código. El código específico de la plataforma entró en sus propias carpetas de proyecto dentro del árbol de código. El código común vivía dentro del árbol según la capa a la que pertenecía.
Tuvimos una compilación condicional, basada en la plataforma que se está construyendo. Mantener eso fue una molestia, pero solo tuvo que hacerse cuando se agregaron nuevos módulos en la capa específica de la plataforma.
Tener todo el código en un único repositorio nos facilitó la corrección de errores en múltiples plataformas y lanzamientos al mismo tiempo. Teníamos un entorno de construcción automatizado para que todas las plataformas sirvieran de respaldo en caso de que un nuevo código rompiera una plataforma supuestamente no relacionada.
Tratamos de desalentarlo, pero habría casos en los que una plataforma necesitara una solución basada en un error específico de la plataforma que de otro modo estaría en un código común. Si pudiéramos anular la compilación condicionalmente sin hacer que el módulo se vea fugitivo, lo haríamos primero. Si no, moveríamos el módulo fuera del territorio común y lo empujaríamos a una plataforma específica.
Para la base de datos, teníamos algunas tablas que tenían columnas / modificaciones específicas de la plataforma. Nos aseguraríamos de que cada versión de plataforma de la tabla cumpliera con un nivel básico de funcionalidad para que el código común pudiera hacer referencia a ella sin preocuparse por las dependencias de la plataforma. Las consultas / manipulaciones específicas de la plataforma se introdujeron en las capas del proyecto de la plataforma.
Entonces, para responder a sus preguntas:
fuente
Trabajé durante muchos años en una aplicación de Administración de Pensiones que tenía problemas similares. Los planes de pensiones son muy diferentes entre las compañías y requieren un conocimiento altamente especializado para implementar la lógica de cálculo e informes y también un diseño de datos muy diferente. Solo puedo dar una breve descripción de parte de la arquitectura, pero tal vez dará suficiente idea.
Teníamos 2 equipos separados: un equipo de desarrollo central , que era responsable del código del sistema central (que sería el 80% del código compartido anteriormente) y un equipo de implementación , que tenía experiencia en el dominio de los sistemas de pensiones y era responsable del aprendizaje del cliente. requisitos y scripts de codificación e informes para el cliente.
Teníamos todas nuestras tablas definidas por Xml (esto antes del momento en que los marcos de entidades eran probados y comunes). El equipo de implementación diseñaría todas las tablas en Xml, y se podría solicitar a la aplicación principal que genere todas las tablas en Xml. También hubo archivos de script VB asociados, Crystal Reports, documentos de Word, etc. para cada cliente. (También había un modelo de herencia integrado en Xml para permitir la reutilización de otras implementaciones).
La aplicación principal (una aplicación para todos los clientes) almacenaba en caché todas las cosas específicas del cliente cuando llegaba una solicitud para ese cliente, y generaba un objeto de datos común (algo así como un conjunto de registros ADO remotos), que podía ser serializado y pasado alrededor.
Este modelo de datos es menos hábil que los objetos de entidad / dominio, pero es muy flexible, universal y puede procesarse mediante un conjunto de código central. Quizás en su caso, podría definir sus objetos de entidad base con solo los campos comunes y tener un Diccionario adicional para campos personalizados (agregue algún tipo de conjunto de descriptores de datos a su objeto de entidad para que tenga metadatos para los campos personalizados. )
Teníamos repositorios de origen separados para el código del sistema central y para el código de implementación.
Nuestro sistema central en realidad tenía muy poca lógica comercial, aparte de algunos módulos de cálculo comunes muy estándar. El sistema central funcionaba como: generador de pantalla, corredor de script, generador de informes, acceso a datos y capa de transporte.
Segmentar la lógica central y la lógica personalizada es un desafío difícil. Sin embargo, siempre sentimos que era mejor tener un sistema central ejecutando múltiples clientes, en lugar de tener múltiples copias del sistema ejecutándose para cada cliente.
fuente
Trabajé en un sistema más pequeño (20 kloc) y descubrí que la DI y la configuración son excelentes formas de gestionar las diferencias entre los clientes, pero no lo suficiente como para evitar bifurcar el sistema. La base de datos se divide entre una parte específica de la aplicación, que tiene un esquema fijo, y la parte dependiente del cliente, que se define a través de un documento de configuración XML personalizado.
Hemos mantenido una única rama en mercurial que está configurada como si fuera entregable, pero marcada y configurada para un cliente ficticio. Las correcciones de errores se incorporan a ese proyecto, y el nuevo desarrollo de la funcionalidad principal solo ocurre allí. Las versiones para clientes reales son ramificaciones de eso, almacenadas en sus propios repositorios. Hacemos un seguimiento de los grandes cambios en el código a través de números de versión escritos manualmente y rastreamos las correcciones de errores utilizando números de confirmación.
fuente
Me temo que no tengo experiencia directa del problema que usted describe, pero tengo algunos comentarios.
La segunda opción, reunir el código en un repositorio central (tanto como sea posible), y diseñar arquitecturas para la personalización (de nuevo, tanto como sea posible) es casi seguro el camino a largo plazo.
El problema es cómo planea llegar allí y cuánto tiempo llevará.
En esta situación, probablemente esté bien (temporalmente) tener más de una copia de la aplicación en el repositorio a la vez.
Esto le permitirá pasar gradualmente a una arquitectura que admita directamente la personalización sin tener que hacerlo de una sola vez.
fuente
Estoy seguro de que cualquiera de esos problemas se puede resolver, uno tras otro. Si se atasca, pregunte aquí o en SO sobre el problema específico.
Como otros han señalado, tener una base de código central / un repositorio es la opción que debería preferir. Intento responder tu pregunta de ejemplo.
Hay algunas posibilidades, todas las he visto en sistemas del mundo real. Cuál elegir depende de su situación:
introduzca una tabla "CustomAttributes" (que describe nombres y tipos) y "CustomAttributeValues" (para los valores, por ejemplo, almacenados como una representación de cadena, incluso si son números). Eso permitirá agregar dichos atributos en el momento de la instalación o el tiempo de ejecución, teniendo valores individuales para cada cliente. No insista en que cada atributo personalizado se modele "visiblemente" en su modelo de datos.
ahora debería estar claro cómo usar eso en el código: tenga solo un código general para acceder a esas tablas y un código individual (tal vez en una DLL de complemento separada, que depende de usted) para interpretar esos atributos correctamente
Y para un código específico: también puede intentar introducir un lenguaje de secuencias de comandos en su producto, especialmente para agregar secuencias de comandos específicas del cliente. De esa manera, no solo crea una línea clara entre su código y el código específico del cliente, sino que también puede permitir que sus clientes personalicen el sistema en cierta medida por sí mismos.
fuente
Solo he construido una de esas aplicaciones. Yo diría que el 90% de las unidades vendidas se vendieron tal cual, sin modificaciones. Cada cliente tenía su propia máscara personalizada y nosotros entregamos el sistema dentro de esa máscara. Cuando entró un mod que afectó las secciones principales, intentamos usar la ramificación IF . Cuando entró el mod # 2 para la misma sección, cambiamos a la lógica CASE que permitía una futura expansión. Esto parecía manejar la mayoría de las solicitudes menores.
Cualquier otra solicitud personalizada menor se manejó implementando la lógica del caso.
Si los mods fueran dos radicales, construimos un clon (incluir por separado) y envolvemos un CASO a su alrededor para incluir el módulo diferente.
Las correcciones de errores y modificaciones en el núcleo afectaron a todos los usuarios. Probamos exhaustivamente en desarrollo antes de pasar a producción. Siempre enviamos notificaciones por correo electrónico que acompañaban cualquier cambio y NUNCA, NUNCA, NUNCA publicamos cambios de producción los viernes ... NUNCA.
Nuestro entorno era Classic ASP y SQL Server. NO éramos una tienda de códigos de espagueti ... Todo era modular usando Incluye, Subrutinas y Funciones.
fuente
Cuando me pidan que comience el desarrollo de B, que comparte el 80% de la funcionalidad con A, haré lo siguiente:
Elegiste 1, y no parece encajar bien con tu situación. Tu misión es predecir cuál de 2 y 3 es mejor.
fuente