Estou trabalhando em um grande projeto de software altamente personalizado para vários clientes em todo o mundo. Isso significa que talvez tenhamos 80% de código comum entre os vários clientes, mas também muitos códigos que precisam ser alterados de um cliente para outro. No passado, fizemos nosso desenvolvimento em repositórios separados (SVN) e, quando um novo projeto foi iniciado (temos poucos, mas grandes clientes), criamos outro repositório com base em qualquer projeto passado que possua a melhor base de código para nossas necessidades. Isso funcionou no passado, mas tivemos vários problemas:
- Os erros corrigidos em um repositório não são corrigidos em outros repositórios. Isso pode ser um problema de organização, mas acho difícil corrigir e corrigir um bug em 5 repositórios diferentes, tendo em mente que a equipe que mantém esse repositório pode estar em outra parte do mundo e não temos seu ambiente de teste , nem conhece sua programação ou quais requisitos eles têm (um "bug" em um país pode ser um "recurso" em outro).
- Os recursos e aprimoramentos feitos para um projeto, que também podem ser úteis para outro projeto, são perdidos ou, se usados em outro projeto, muitas vezes causam grandes dores de cabeça, mesclando-os de uma base de código para outra (já que os dois ramos podem ter sido desenvolvidos independentemente por um ano )
- As refatorações e aprimoramentos de código feitos em uma ramificação de desenvolvimento são perdidos ou causam mais danos do que benefícios, se você precisar mesclar todas essas alterações entre as ramificações.
Agora estamos discutindo como resolver esses problemas e, até agora, surgiram as seguintes idéias sobre como resolver isso:
Mantenha o desenvolvimento em ramificações separadas, mas organize-se melhor com um repositório central onde as correções gerais de erros são mescladas e com todos os projetos que mesclam as alterações desse repositório central em seus próprios, regularmente (por exemplo, diariamente). Isso requer muita disciplina e muito esforço para se fundir entre os ramos. Portanto, não estou convencido de que funcione e que possamos manter essa disciplina, especialmente quando a pressão do tempo se aproxima.
Abandone as ramificações de desenvolvimento separadas e tenha um repositório de código central onde todo o nosso código vive e faça nossa personalização com módulos e opções de configuração conectáveis. Já estamos usando contêineres de injeção de dependência para resolver dependências em nosso código e seguimos o padrão MVVM na maioria de nosso código para separar de maneira limpa a lógica comercial da nossa interface do usuário.
A segunda abordagem parece ser mais elegante, mas temos muitos problemas não resolvidos nessa abordagem. Por exemplo: como lidar com alterações / adições no seu modelo / banco de dados. Estamos usando o .NET com Entity Framework para ter entidades fortemente tipadas. Não vejo como podemos lidar com propriedades que são necessárias para um cliente, mas inúteis para outro cliente sem sobrecarregar nosso modelo de dados. Estamos pensando em resolver isso no banco de dados usando tabelas de satélite (com tabelas separadas em que nossas colunas extras para uma entidade específica vivem com um mapeamento 1: 1 para a entidade original), mas esse é apenas o banco de dados. Como você lida com isso no código? Nosso modelo de dados reside em uma biblioteca central que não poderíamos estender para cada cliente usando essa abordagem.
Tenho certeza de que não somos a única equipe que está lutando com esse problema e estou chocado ao encontrar tão pouco material sobre o assunto.
Então, minhas perguntas são as seguintes:
- Que experiência você tem com software altamente personalizado, qual abordagem você escolheu e como funcionou para você?
- Que abordagem você recomenda e por quê? Existe uma abordagem melhor?
- Existem bons livros ou artigos sobre o assunto que você pode recomendar?
- Você tem recomendações específicas para o nosso ambiente técnico (.NET, Entity Framework, WPF, DI)?
Editar:
Obrigado por todas as sugestões. A maioria das idéias corresponde às que já tínhamos em nossa equipe, mas é realmente útil ver a experiência que você teve com elas e dicas para implementá-las melhor.
Ainda não tenho certeza de que caminho seguiremos e não estou tomando a decisão (sozinho), mas transmitirei isso na minha equipe e tenho certeza de que será útil.
No momento, o teor parece ser um único repositório usando vários módulos específicos do cliente. Não tenho certeza de que nossa arquitetura esteja de acordo com isso ou quanto temos que investir para ajustá-la; portanto, algumas coisas podem viver em repositórios separados por um tempo, mas acho que é a única solução de longo prazo que funcionará.
Então, obrigado novamente por todas as respostas!
Respostas:
Parece que o problema fundamental não é apenas a manutenção do repositório de código, mas a falta de arquitetura adequada .
Uma estrutura ou biblioteca padrão abrange a primeira, enquanto a segunda seria implementada como complementos (plugins, subclasses, DI, o que fizer sentido para a estrutura do código).
Um sistema de controle de origem que gerencia filiais e desenvolvimento distribuído provavelmente ajudaria também; Sou fã do Mercurial, outros preferem o Git. A estrutura seria a ramificação principal, cada sistema personalizado seria sub-ramificações, por exemplo.
As tecnologias específicas usadas para implementar o sistema (.NET, WPF, qualquer que seja) são em grande parte sem importância.
Conseguir isso direito não é fácil , mas é fundamental para a viabilidade a longo prazo. E, é claro, quanto mais você esperar, maior será a dívida técnica com a qual terá de lidar.
Você pode achar útil o livro Arquitetura de software: princípios e padrões organizacionais .
Boa sorte!
fonte
Uma empresa em que trabalhei tinha o mesmo problema, e a abordagem para resolvê-lo foi a seguinte: Foi criada uma estrutura comum para todos os novos projetos; isso inclui todas as coisas que precisam ser iguais em todos os projetos. Por exemplo, ferramentas de geração de formulários, exportação para Excel, registro em log. Esforço foi feito para garantir que essa estrutura comum fosse aprimorada apenas (quando um novo projeto precisa de novos recursos), mas nunca bifurcada.
Com base nessa estrutura, o código específico do cliente foi mantido em repositórios separados. Quando útil ou necessário, as correções e melhorias são copiadas e coladas entre os projetos (com todas as advertências descritas na pergunta). Melhorias globalmente úteis entram na estrutura comum, no entanto.
Ter tudo em uma base de código comum para todos os clientes tem algumas vantagens, mas, por outro lado, a leitura do código se torna difícil quando existem inúmeros
if
s para fazer com que o programa se comporte de maneira diferente para cada cliente.EDIT: Uma anedota para tornar isso mais compreensível:
fonte
Um dos projetos nos quais trabalhei deu suporte a várias plataformas (mais de 5) em um grande número de lançamentos de produtos. Muitos dos desafios que você está descrevendo foram coisas que enfrentamos, embora de uma maneira um pouco diferente. Como tínhamos um banco de dados proprietário, não tivemos os mesmos tipos de problemas nessa arena.
Nossa estrutura era semelhante à sua, mas tínhamos um único repositório para o nosso código. O código específico da plataforma foi para as próprias pastas do projeto na árvore de códigos. O código comum residia na árvore com base em qual camada ela pertencia.
Tivemos uma compilação condicional, com base na plataforma que está sendo construída. Manter isso meio doloroso, mas só precisava ser feito quando novos módulos foram adicionados na camada específica da plataforma.
Ter todo o código em um único repositório facilitou a correção de bugs em várias plataformas e lançamentos ao mesmo tempo. Tínhamos um ambiente de compilação automatizado para todas as plataformas servirem de suporte para o caso de um novo código quebrar uma suposta plataforma não relacionada.
Tentamos desencorajá-lo, mas havia casos em que uma plataforma precisava de uma correção com base em um bug específico da plataforma que estava em um código comum. Se pudéssemos substituir condicionalmente a compilação sem fazer com que o módulo parecesse imprudente, faríamos isso primeiro. Caso contrário, removeríamos o módulo do território comum e o empurraríamos para uma plataforma específica.
Para o banco de dados, tínhamos algumas tabelas com colunas / modificações específicas da plataforma. Garantiríamos que todas as versões da tabela da plataforma atendessem a um nível básico de funcionalidade, para que o código comum pudesse fazer referência a ela sem se preocupar com as dependências da plataforma. As consultas / manipulações específicas da plataforma foram enviadas para as camadas do projeto da plataforma.
Então, para responder suas perguntas:
fonte
Trabalhei por muitos anos em um aplicativo de Administração de Pensões que apresentava problemas semelhantes. Os planos de pensão são muito diferentes entre as empresas e exigem conhecimento altamente especializado para implementar lógica e relatórios de cálculo e também design de dados muito diferentes. Só posso dar uma breve descrição de parte da arquitetura, mas talvez ela dê o suficiente da idéia.
Tínhamos duas equipes separadas: uma equipe de desenvolvimento principal, responsável pelo código do sistema principal (que seria seu código compartilhado de 80% acima) e uma equipe de implementação , que possuía experiência em domínio em sistemas de pensão e responsável pelo aprendizado do cliente requisitos e scripts e relatórios de codificação para o cliente.
Tínhamos todas as nossas tabelas definidas por Xml (isso antes do tempo em que estruturas de entidade eram testadas e comuns). A equipe de implementação projetaria todas as tabelas em Xml, e o aplicativo principal poderia ser solicitado a gerar todas as tabelas em Xml. Também havia arquivos de script VB associados, Crystal Reports, documentos do Word etc. para cada cliente. (Também havia um modelo de herança incorporado no Xml para permitir a reutilização de outras implementações).
O aplicativo principal (um aplicativo para todos os clientes), armazenaria em cache todo o material específico do cliente quando chegasse uma solicitação e gerasse um objeto de dados comum (como um conjunto de registros ADO remotos), que poderia ser serializado e transmitido por aí.
Esse modelo de dados é menos liso que os objetos de entidade / domínio, mas é altamente flexível, universal e pode ser processado por um conjunto de códigos principais. Talvez no seu caso, você possa definir seus objetos de entidade base apenas com os campos comuns e ter um Dicionário adicional para campos personalizados (adicione algum tipo de conjunto de descritores de dados ao seu objeto de entidade para que ele possua metadados para os campos personalizados. )
Tínhamos repositórios de origem separados para o código do sistema principal e para o código de implementação.
Na verdade, nosso sistema principal tinha muito pouca lógica de negócios, além de alguns módulos comuns de cálculo comuns. O sistema principal funcionava como: gerador de tela, gerenciador de scripts, gerador de relatórios, acesso a dados e camada de transporte.
Segmentar a lógica principal e a lógica personalizada é um desafio difícil. No entanto, sempre achamos que era melhor ter um sistema principal executando vários clientes, em vez de várias cópias do sistema executando para cada cliente.
fonte
Trabalhei em um sistema menor (20 kloc) e descobri que o DI e a configuração são ótimas maneiras de gerenciar diferenças entre os clientes, mas não o suficiente para evitar a bifurcação do sistema. O banco de dados é dividido entre uma parte específica do aplicativo, que possui um esquema fixo, e a parte dependente do cliente, definida por meio de um documento de configuração XML personalizado.
Mantivemos uma única filial no mercurial configurada como se fosse entregável, mas com marca e configurada para um cliente fictício. As correções de erros são incorporadas nesse projeto, e o novo desenvolvimento da funcionalidade principal acontece apenas lá. As liberações para clientes reais são ramificações disso, armazenadas em seus próprios repositórios. Nós acompanhamos grandes alterações no código por meio de números de versão escritos manualmente e acompanhamos correções de erros usando números de confirmação.
fonte
Receio não ter experiência direta com o problema que você descreve, mas tenho alguns comentários.
A segunda opção, reunir o código em um repositório central (tanto quanto possível) e arquitetar para personalização (novamente, tanto quanto possível) é quase certamente o caminho a longo prazo.
O problema é como você planeja chegar lá e quanto tempo levará.
Nessa situação, provavelmente não há problema em (temporariamente) ter mais de uma cópia do aplicativo no repositório por vez.
Isso permitirá que você mude gradualmente para uma arquitetura que suporte diretamente a personalização sem precisar fazê-lo de uma só vez.
fonte
Estou certo de que qualquer um desses problemas pode ser resolvido, um após o outro. Se você ficar preso, pergunte aqui no SO sobre o problema específico.
Como outros já apontaram, ter uma base de código central / um repositório é a opção que você prefere. Eu tento responder a sua pergunta de exemplo.
Existem algumas possibilidades, todas elas que eu já vi em sistemas do mundo real. Qual escolher depende da sua situação:
introduza as tabelas "CustomAttributes" (descrevendo nomes e tipos) e "CustomAttributeValues" (para os valores, por exemplo, armazenados como uma representação de sequência, mesmo que sejam números). Isso permitirá adicionar esses atributos no momento da instalação ou no tempo de execução, tendo valores individuais para cada cliente. Não insista em que cada atributo personalizado seja modelado "visivelmente" em seu modelo de dados.
agora deve ficar claro como usar isso no código: tenha apenas código geral para acessar essas tabelas e código individual (talvez em uma DLL de plug-in separada, que é sua) para interpretar esses atributos corretamente
E para código específico: você também pode tentar introduzir uma linguagem de script em seu produto, especialmente para adicionar scripts específicos ao cliente. Dessa forma, você não apenas cria uma linha clara entre seu código e o código específico do cliente, mas também pode permitir que seus clientes personalizem o sistema até certo ponto por si próprios.
fonte
Eu apenas criei um desses aplicativos. Eu diria que 90% das unidades vendidas foram vendidas como estão, sem modificações. Cada cliente tinha sua própria capa personalizada e atendemos o sistema nessa capa. Quando um mod chegou e afetou as seções principais, tentamos usar a ramificação IF . Quando o mod # 2 entrou na mesma seção, mudamos para a lógica CASE, que permitia uma expansão futura. Isso parecia lidar com a maioria dos pedidos menores.
Quaisquer pedidos personalizados menores adicionais foram tratados implementando a lógica de Caso.
Se os mods fossem dois radicais, construímos um clone (inclusão separada) e envolvemos um CASE em torno dele para incluir o módulo diferente.
As correções e modificações no núcleo afetavam todos os usuários. Testamos minuciosamente o desenvolvimento antes de iniciar a produção. Sempre enviamos notificações por e-mail que acompanharam qualquer alteração e NUNCA, NUNCA, NUNCA postou alterações de produção às sextas-feiras ... NUNCA.
Nosso ambiente era o ASP clássico e o SQL Server. Nós não éramos uma loja de códigos de espaguete ... Tudo era modular usando Inclui, Sub-rotinas e Funções.
fonte
Quando me pedem para iniciar o desenvolvimento de B, que está compartilhando 80% da funcionalidade com A, eu:
Você escolheu 1, e isso não parece se encaixar bem na sua situação. Sua missão é prever qual dos 2 e 3 é o mais adequado.
fonte